commit 5db26aa10fc62ac8acd44daf58d55e7a52991318 Author: Ubuntu Date: Tue Feb 24 16:10:30 2026 +0000 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..12c23ae --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +node_modules +.next +.dist +build +coverage +.env +.env.* +apps/api/.env +apps/web/.env +apps/worker/.env +.DS_Store +pnpm-debug.log* diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..6e1a9fe --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +shared-workspace-lockfile=true diff --git a/apps/api/README.md b/apps/api/README.md new file mode 100644 index 0000000..e91648c --- /dev/null +++ b/apps/api/README.md @@ -0,0 +1,23 @@ +# API (Express) + +## Variables d'environnement + +Créer un fichier `apps/api/.env` à partir de `.env.example`. + +Exemple : + +``` +DATABASE_URL=postgresql://postgres@localhost:5432/localiztoi_stock +PORT=4000 +``` + +## Démarrer + +Depuis la racine du repo : + +- `pnpm -F @localiztoi/api dev` + +## Migrations (Drizzle) + +- Générer : `pnpm -F @localiztoi/api db:generate` +- Appliquer : `pnpm -F @localiztoi/api db:migrate` diff --git a/apps/api/dist/db/index.js b/apps/api/dist/db/index.js new file mode 100644 index 0000000..b279fb4 --- /dev/null +++ b/apps/api/dist/db/index.js @@ -0,0 +1,23 @@ +import { drizzle } from 'drizzle-orm/node-postgres'; +import pg from 'pg'; +const { Pool } = pg; +let singletonPool = null; +export const getPool = () => singletonPool; +export const getDb = () => { + const databaseUrl = process.env.DATABASE_URL; + if (!databaseUrl) { + throw new Error('DATABASE_URL is required'); + } + if (!singletonPool) { + singletonPool = new Pool({ + connectionString: databaseUrl, + max: 10, + idleTimeoutMillis: 30_000, + connectionTimeoutMillis: 5_000 + }); + singletonPool.on('error', (err) => { + console.error('[db] Pool background error', err.message); + }); + } + return drizzle(singletonPool); +}; diff --git a/apps/api/dist/db/schema/amazonPollLog.js b/apps/api/dist/db/schema/amazonPollLog.js new file mode 100644 index 0000000..0cbf0af --- /dev/null +++ b/apps/api/dist/db/schema/amazonPollLog.js @@ -0,0 +1,10 @@ +import { integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'; +export const amazonPollLog = pgTable('amazon_poll_log', { + id: uuid('id').primaryKey().defaultRandom(), + startedAt: timestamp('started_at', { withTimezone: true }).notNull().defaultNow(), + finishedAt: timestamp('finished_at', { withTimezone: true }), + ordersFound: integer('orders_found').notNull().default(0), + ordersImported: integer('orders_imported').notNull().default(0), + error: text('error'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), +}); diff --git a/apps/api/dist/db/schema/apiKeys.js b/apps/api/dist/db/schema/apiKeys.js new file mode 100644 index 0000000..093e90c --- /dev/null +++ b/apps/api/dist/db/schema/apiKeys.js @@ -0,0 +1,15 @@ +import { pgTable, text, timestamp, uniqueIndex, uuid } from 'drizzle-orm/pg-core'; +export const apiKeys = pgTable('api_keys', { + id: uuid('id').primaryKey().defaultRandom(), + provider: text('provider').notNull(), + label: text('label').notNull(), + value: text('value').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }) + .notNull() + .defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }) + .notNull() + .defaultNow() +}, (t) => ({ + providerLabelUniqueIdx: uniqueIndex('api_keys_provider_label_unique').on(t.provider, t.label) +})); diff --git a/apps/api/dist/db/schema/index.js b/apps/api/dist/db/schema/index.js new file mode 100644 index 0000000..5efc989 --- /dev/null +++ b/apps/api/dist/db/schema/index.js @@ -0,0 +1,7 @@ +export * from './skuMappings.js'; +export * from './orders.js'; +export * from './orderItems.js'; +export * from './orderImeis.js'; +export * from './apiKeys.js'; +export * from './amazonPollLog.js'; +export * from './users.js'; diff --git a/apps/api/dist/db/schema/orderImeis.js b/apps/api/dist/db/schema/orderImeis.js new file mode 100644 index 0000000..6093dd9 --- /dev/null +++ b/apps/api/dist/db/schema/orderImeis.js @@ -0,0 +1,27 @@ +import { integer, pgTable, text, timestamp, uniqueIndex, uuid } from 'drizzle-orm/pg-core'; +import { orders } from './orders.js'; +export const orderImeis = pgTable('order_imeis', { + id: uuid('id').primaryKey().defaultRandom(), + orderId: uuid('order_id') + .notNull() + .references(() => orders.id, { onDelete: 'cascade' }), + imei: text('imei').notNull(), + fotaModel: text('fota_model'), + fotaSerial: text('fota_serial'), + fotaCurrentFirmware: text('fota_current_firmware'), + fotaActivityStatus: integer('fota_activity_status'), + fotaSeenAt: text('fota_seen_at'), + fotaCompanyId: integer('fota_company_id'), + fotaCompanyName: text('fota_company_name'), + fotaGroupId: integer('fota_group_id'), + fotaGroupName: text('fota_group_name'), + fotaLookupError: text('fota_lookup_error'), + fotaLastLookupAt: timestamp('fota_last_lookup_at', { withTimezone: true }), + fotaMovedAt: timestamp('fota_moved_at', { withTimezone: true }), + fotaMoveError: text('fota_move_error'), + createdAt: timestamp('created_at', { withTimezone: true }) + .notNull() + .defaultNow() +}, (t) => ({ + orderImeiUniqueIdx: uniqueIndex('order_imeis_order_id_imei_unique').on(t.orderId, t.imei) +})); diff --git a/apps/api/dist/db/schema/orderItems.js b/apps/api/dist/db/schema/orderItems.js new file mode 100644 index 0000000..dfe5f10 --- /dev/null +++ b/apps/api/dist/db/schema/orderItems.js @@ -0,0 +1,15 @@ +import { integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'; +import { orders } from './orders.js'; +export const orderItems = pgTable('order_items', { + id: uuid('id').primaryKey().defaultRandom(), + orderId: uuid('order_id') + .notNull() + .references(() => orders.id, { onDelete: 'cascade' }), + amazonSku: text('amazon_sku').notNull(), + amazonOrderItemId: text('amazon_order_item_id'), + quantity: integer('quantity').notNull(), + title: text('title'), + createdAt: timestamp('created_at', { withTimezone: true }) + .notNull() + .defaultNow() +}); diff --git a/apps/api/dist/db/schema/orders.js b/apps/api/dist/db/schema/orders.js new file mode 100644 index 0000000..dde541b --- /dev/null +++ b/apps/api/dist/db/schema/orders.js @@ -0,0 +1,34 @@ +import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'; +export const orders = pgTable('orders', { + id: uuid('id').primaryKey().defaultRandom(), + orderRef: text('order_ref').notNull(), + status: text('status').notNull().default('new'), + axonautDestockedAt: timestamp('axonaut_destocked_at', { withTimezone: true }), + axonautDestockError: text('axonaut_destock_error'), + colissimoTrackingNumber: text('colissimo_tracking_number'), + colissimoLabelUrl: text('colissimo_label_url'), + colissimoShippedAt: timestamp('colissimo_shipped_at', { withTimezone: true }), + colissimoError: text('colissimo_error'), + amazonTrackingConfirmedAt: timestamp('amazon_tracking_confirmed_at', { withTimezone: true }), + amazonTrackingError: text('amazon_tracking_error'), + shippingName: text('shipping_name'), + shippingFirstName: text('shipping_first_name'), + shippingLastName: text('shipping_last_name'), + shippingLine1: text('shipping_line1'), + shippingLine2: text('shipping_line2'), + shippingLine3: text('shipping_line3'), + shippingCity: text('shipping_city'), + shippingZipCode: text('shipping_zip_code'), + shippingCountryCode: text('shipping_country_code'), + shippingState: text('shipping_state'), + shippingPhone: text('shipping_phone'), + shippingAddressType: text('shipping_address_type'), + shippingFetchedAt: timestamp('shipping_fetched_at', { withTimezone: true }), + shippingFetchError: text('shipping_fetch_error'), + createdAt: timestamp('created_at', { withTimezone: true }) + .notNull() + .defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }) + .notNull() + .defaultNow() +}); diff --git a/apps/api/dist/db/schema/skuMappings.js b/apps/api/dist/db/schema/skuMappings.js new file mode 100644 index 0000000..2fd758d --- /dev/null +++ b/apps/api/dist/db/schema/skuMappings.js @@ -0,0 +1,16 @@ +import { pgTable, text, timestamp, uniqueIndex, uuid } from 'drizzle-orm/pg-core'; +export const skuMappings = pgTable('sku_mappings', { + id: uuid('id').primaryKey().defaultRandom(), + amazonSku: text('amazon_sku').notNull(), + enterpriseSku: text('enterprise_sku').notNull(), + expectedFotaModel: text('expected_fota_model').notNull(), + axonautProductInternalId: text('axonaut_product_internal_id').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }) + .notNull() + .defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }) + .notNull() + .defaultNow() +}, (t) => ({ + amazonSkuUniqueIdx: uniqueIndex('sku_mappings_amazon_sku_unique').on(t.amazonSku) +})); diff --git a/apps/api/dist/db/schema/users.js b/apps/api/dist/db/schema/users.js new file mode 100644 index 0000000..0a27953 --- /dev/null +++ b/apps/api/dist/db/schema/users.js @@ -0,0 +1,9 @@ +import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'; +export const users = pgTable('users', { + id: uuid('id').primaryKey().defaultRandom(), + username: text('username').notNull().unique(), + passwordHash: text('password_hash').notNull(), + displayName: text('display_name').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), +}); diff --git a/apps/api/dist/index.js b/apps/api/dist/index.js new file mode 100644 index 0000000..55cfb4a --- /dev/null +++ b/apps/api/dist/index.js @@ -0,0 +1,2376 @@ +import cors from 'cors'; +import express from 'express'; +import axios from 'axios'; +import jwt from 'jsonwebtoken'; +import bcrypt from 'bcryptjs'; +import cookieParser from 'cookie-parser'; +import { and, desc, eq, inArray, sql } from 'drizzle-orm'; +import { orderCreateSchema, scanImeiSchema, skuMappingSchema, apiKeyCreateSchema, apiKeyUpdateSchema, loginSchema, registerSchema, changePasswordSchema } from '@localiztoi/shared'; +import { getDb, getPool } from './db/index.js'; +import { orderImeis, orderItems, orders, skuMappings, apiKeys, amazonPollLog, users } from './db/schema/index.js'; +const app = express(); +// CORS avec credentials (cookies) +const CORS_ORIGIN = process.env.CORS_ORIGIN ?? 'http://localhost:3000'; +const allowedOrigins = CORS_ORIGIN.split(',').map((o) => o.trim()); +app.use(cors({ + origin: (origin, callback) => { + if (!origin || allowedOrigins.includes(origin)) { + callback(null, true); + } + else { + callback(new Error('Not allowed by CORS')); + } + }, + credentials: true +})); +app.use(express.json()); +app.use(cookieParser()); +// --- JWT helpers --- +const JWT_SECRET = process.env.JWT_SECRET; +const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN ?? '7d'; +const COOKIE_SECURE = process.env.COOKIE_SECURE === 'true'; +const signToken = (user) => { + if (!JWT_SECRET) + throw new Error('JWT_SECRET is not configured'); + return jwt.sign({ sub: user.id, username: user.username, displayName: user.displayName }, JWT_SECRET, { + expiresIn: JWT_EXPIRES_IN + }); +}; +const setAuthCookie = (res, token) => { + res.cookie('auth_token', token, { + httpOnly: true, + secure: COOKIE_SECURE, + sameSite: 'lax', + maxAge: 7 * 24 * 60 * 60 * 1000, // 7 jours + path: '/' + }); +}; +const verifyToken = (token) => { + if (!JWT_SECRET) + return null; + try { + const payload = jwt.verify(token, JWT_SECRET); + return { + id: payload.sub, + username: payload.username, + displayName: payload.displayName + }; + } + catch { + return null; + } +}; +const teltonikaBaseUrl = 'https://api.teltonika.lt'; +const getTeltonikaAuthHeaders = () => { + const token = process.env.TELTONIKA_FOTA_API_TOKEN; + if (!token) { + throw new Error('TELTONIKA_FOTA_API_TOKEN is required'); + } + const userAgent = process.env.TELTONIKA_FOTA_USER_AGENT; + if (!userAgent) { + throw new Error('TELTONIKA_FOTA_USER_AGENT is required'); + } + return { + Authorization: `Bearer ${token}`, + 'User-Agent': userAgent, + Accept: 'application/json', + 'Content-Type': 'application/json' + }; +}; +const teltonikaClient = axios.create({ + baseURL: teltonikaBaseUrl, + timeout: 15000 +}); +const toMaybeInt64 = (value) => { + const cleaned = value.trim(); + if (!cleaned) + return null; + const n = Number(cleaned); + if (!Number.isFinite(n)) + return null; + if (!Number.isInteger(n)) + return null; + return n; +}; +const isMaybeImei = (value) => { + const cleaned = value.trim(); + if (!cleaned) + return false; + if (!/^[0-9]+$/.test(cleaned)) + return false; + return cleaned.length === 15; +}; +const normalizeFotaActivityStatus = (value) => { + if (value === null || typeof value === 'undefined') { + return null; + } + if (typeof value === 'number' && Number.isFinite(value)) { + return value; + } + if (typeof value === 'string') { + const cleaned = value.trim().toLowerCase(); + if (cleaned === 'online') + return 2; + if (cleaned === 'offline') + return 1; + if (cleaned === 'inactive') + return 0; + const asNumber = Number(cleaned); + if (Number.isFinite(asNumber) && Number.isInteger(asNumber)) { + return asNumber; + } + } + return null; +}; +const lookupTeltonikaDeviceByImei = async (imei) => { + const numericImei = toMaybeInt64(imei); + if (numericImei === null) { + throw new Error('invalid_imei'); + } + const res = await teltonikaClient.get(`/devices/${numericImei}`, { + headers: getTeltonikaAuthHeaders() + }); + return res.data; +}; +const extractFirstDeviceFromPagedResult = (payload) => { + if (!payload) + return null; + if (Array.isArray(payload)) + return payload[0] ?? null; + if (Array.isArray(payload.data)) + return payload.data[0] ?? null; + if (Array.isArray(payload.items)) + return payload.items[0] ?? null; + if (Array.isArray(payload.results)) + return payload.results[0] ?? null; + return null; +}; +const resolveTeltonikaDeviceByIdentifier = async (identifier) => { + const cleaned = identifier.trim(); + if (!cleaned) { + throw new Error('invalid_identifier'); + } + if (isMaybeImei(cleaned)) { + const numericImei = toMaybeInt64(cleaned); + if (numericImei === null) { + throw new Error('invalid_imei'); + } + const device = await lookupTeltonikaDeviceByImei(cleaned); + return { + imei: String(numericImei), + device + }; + } + const res = await teltonikaClient.get('/devices', { + headers: getTeltonikaAuthHeaders(), + params: { + query: cleaned, + query_field: 'serial', + per_page: 1, + page: 1 + } + }); + const device = extractFirstDeviceFromPagedResult(res.data); + const deviceImei = device?.imei ?? device?.device_imei; + const numeric = typeof deviceImei === 'number' ? deviceImei : toMaybeInt64(String(deviceImei ?? '')); + if (numeric === null) { + throw new Error('device_not_found'); + } + return { + imei: String(numeric), + device + }; +}; +app.get('/health', (_req, res) => { + res.json({ ok: true }); +}); +// --- Auth routes --- +app.post('/auth/login', async (req, res) => { + const parsed = loginSchema.safeParse(req.body); + if (!parsed.success) { + return res.status(400).json({ message: 'invalid_body', issues: parsed.error.issues }); + } + try { + const db = getDb(); + const [user] = await db + .select() + .from(users) + .where(eq(users.username, parsed.data.username)); + if (!user) { + return res.status(401).json({ message: 'invalid_credentials' }); + } + const validPassword = await bcrypt.compare(parsed.data.password, user.passwordHash); + if (!validPassword) { + return res.status(401).json({ message: 'invalid_credentials' }); + } + const authUser = { id: user.id, username: user.username, displayName: user.displayName }; + const token = signToken(authUser); + setAuthCookie(res, token); + return res.json({ user: authUser }); + } + catch (err) { + console.error('POST /auth/login failed', err); + return res.status(500).json({ message: 'internal_error' }); + } +}); +app.post('/auth/register', async (req, res) => { + const parsed = registerSchema.safeParse(req.body); + if (!parsed.success) { + return res.status(400).json({ message: 'invalid_body', issues: parsed.error.issues }); + } + try { + const db = getDb(); + // Vérifier s'il y a déjà des utilisateurs + const [{ count }] = await db.select({ count: sql `count(*)::int` }).from(users); + const isFirstUser = count === 0; + // Si ce n'est pas le premier utilisateur, il faut être authentifié + if (!isFirstUser) { + const token = req.cookies?.auth_token; + const currentUser = token ? verifyToken(token) : null; + if (!currentUser) { + return res.status(401).json({ message: 'auth_required' }); + } + } + const passwordHash = await bcrypt.hash(parsed.data.password, 12); + const [created] = await db + .insert(users) + .values({ + username: parsed.data.username, + passwordHash, + displayName: parsed.data.displayName + }) + .returning(); + const authUser = { id: created.id, username: created.username, displayName: created.displayName }; + // Si c'est le premier utilisateur, le connecter automatiquement + if (isFirstUser) { + const token = signToken(authUser); + setAuthCookie(res, token); + } + return res.status(201).json({ user: authUser }); + } + catch (err) { + const maybePgCode = err?.code; + if (maybePgCode === '23505') { + return res.status(409).json({ message: 'username_taken' }); + } + console.error('POST /auth/register failed', err); + return res.status(500).json({ message: 'internal_error' }); + } +}); +app.post('/auth/logout', (_req, res) => { + res.clearCookie('auth_token', { path: '/' }); + return res.json({ message: 'logged_out' }); +}); +app.get('/auth/me', (req, res) => { + const token = req.cookies?.auth_token; + if (!token) { + return res.status(401).json({ message: 'not_authenticated' }); + } + const user = verifyToken(token); + if (!user) { + res.clearCookie('auth_token', { path: '/' }); + return res.status(401).json({ message: 'invalid_token' }); + } + return res.json({ user }); +}); +// --- Auth middleware for /admin/* --- +app.use('/admin', (req, res, next) => { + const token = req.cookies?.auth_token; + if (!token) { + return res.status(401).json({ message: 'not_authenticated' }); + } + const user = verifyToken(token); + if (!user) { + res.clearCookie('auth_token', { path: '/' }); + return res.status(401).json({ message: 'invalid_token' }); + } + req.user = user; + next(); +}); +app.get('/admin/fota/filter-list', async (req, res) => { + const field = String(req.query.field ?? ''); + const allowedFields = new Set(['company_id', 'group_id']); + if (!allowedFields.has(field)) { + return res.status(400).json({ message: 'invalid_field' }); + } + try { + const result = await teltonikaClient.get('/devices/filterList', { + headers: getTeltonikaAuthHeaders(), + params: { field } + }); + return res.json(result.data); + } + catch (err) { + const error = err; + const message = error?.message ? String(error.message) : 'unknown_error'; + const status = error?.response?.status; + const data = error?.response?.data; + console.error('GET /admin/fota/filter-list failed', { + message, + status, + data + }); + if (message.includes('TELTONIKA_FOTA_API_TOKEN is required')) { + return res.status(500).json({ message: 'missing_teltonika_token' }); + } + if (message.includes('TELTONIKA_FOTA_USER_AGENT is required')) { + return res.status(500).json({ message: 'missing_teltonika_user_agent' }); + } + if (typeof status === 'number') { + return res.status(502).json({ + message: 'teltonika_error', + status, + data + }); + } + return res.status(500).json({ message: 'internal_error', detail: message }); + } +}); +app.get('/admin/fota/lookup', async (req, res) => { + const identifier = String(req.query.identifier ?? '').trim(); + if (!identifier) { + return res.status(400).json({ message: 'identifier_required' }); + } + try { + const resolved = await resolveTeltonikaDeviceByIdentifier(identifier); + const device = resolved.device; + return res.json({ + imei: resolved.imei, + fotaModel: device?.model ?? null, + fotaSerial: device?.serial ?? null, + fotaCurrentFirmware: device?.current_firmware ?? null, + fotaActivityStatus: normalizeFotaActivityStatus(device?.activity_status), + fotaSeenAt: device?.seen_at ?? null, + fotaCompanyName: device?.company_name ?? null, + fotaGroupName: device?.group_name ?? null + }); + } + catch (err) { + const error = err; + const message = error?.message ? String(error.message) : 'unknown_error'; + const status = error?.response?.status; + const data = error?.response?.data; + console.error('GET /admin/fota/lookup failed', { identifier, message, status, data }); + if (message === 'invalid_identifier' || message === 'invalid_imei') { + return res.status(400).json({ message }); + } + if (message === 'device_not_found') { + return res.status(404).json({ message }); + } + if (message.includes('TELTONIKA_FOTA_API_TOKEN is required')) { + return res.status(500).json({ message: 'missing_teltonika_token' }); + } + if (message.includes('TELTONIKA_FOTA_USER_AGENT is required')) { + return res.status(500).json({ message: 'missing_teltonika_user_agent' }); + } + if (typeof status === 'number') { + return res.status(502).json({ message: 'teltonika_error', status, data }); + } + return res.status(500).json({ message: 'internal_error', detail: message }); + } +}); +app.get('/admin/sku-mappings', async (_req, res) => { + try { + const db = getDb(); + const rows = await db.select().from(skuMappings).orderBy(skuMappings.createdAt); + res.json(rows); + } + catch (err) { + // eslint-disable-next-line no-console + console.error('GET /admin/sku-mappings failed', err); + res.status(500).json({ message: 'internal_error' }); + } +}); +app.post('/admin/sku-mappings', async (req, res) => { + const parsed = skuMappingSchema.safeParse(req.body); + if (!parsed.success) { + return res.status(400).json({ + message: 'invalid_body', + issues: parsed.error.issues + }); + } + try { + const db = getDb(); + const [created] = await db + .insert(skuMappings) + .values({ + amazonSku: parsed.data.amazonSku, + enterpriseSku: parsed.data.enterpriseSku, + expectedFotaModel: parsed.data.expectedFotaModel, + axonautProductInternalId: parsed.data.axonautProductInternalId + }) + .returning(); + return res.status(201).json(created); + } + catch (err) { + // eslint-disable-next-line no-console + console.error('POST /admin/sku-mappings failed', err); + return res.status(500).json({ message: 'internal_error' }); + } +}); +app.patch('/admin/sku-mappings/:id', async (req, res) => { + const { id } = req.params; + const patchSchema = skuMappingSchema.partial(); + const parsed = patchSchema.safeParse(req.body); + if (!parsed.success) { + return res.status(400).json({ + message: 'invalid_body', + issues: parsed.error.issues + }); + } + try { + const db = getDb(); + const [updated] = await db + .update(skuMappings) + .set({ + ...parsed.data, + updatedAt: new Date() + }) + .where(eq(skuMappings.id, id)) + .returning(); + if (!updated) { + return res.status(404).json({ message: 'not_found' }); + } + return res.json(updated); + } + catch (err) { + // eslint-disable-next-line no-console + console.error('PATCH /admin/sku-mappings failed', err); + return res.status(500).json({ message: 'internal_error' }); + } +}); +app.delete('/admin/sku-mappings/:id', async (req, res) => { + const { id } = req.params; + try { + const db = getDb(); + const deleted = await db + .delete(skuMappings) + .where(eq(skuMappings.id, id)) + .returning({ id: skuMappings.id }); + if (deleted.length === 0) { + return res.status(404).json({ message: 'not_found' }); + } + return res.status(204).send(); + } + catch (err) { + // eslint-disable-next-line no-console + console.error('DELETE /admin/sku-mappings failed', err); + return res.status(500).json({ message: 'internal_error' }); + } +}); +app.get('/admin/orders', async (_req, res) => { + try { + const db = getDb(); + const rows = await db.select().from(orders).orderBy(desc(orders.createdAt)); + res.json(rows); + } + catch (err) { + // eslint-disable-next-line no-console + console.error('GET /admin/orders failed', err); + res.status(500).json({ message: 'internal_error' }); + } +}); +app.post('/admin/orders', async (req, res) => { + const parsed = orderCreateSchema.safeParse(req.body); + if (!parsed.success) { + return res.status(400).json({ + message: 'invalid_body', + issues: parsed.error.issues + }); + } + try { + const db = getDb(); + const created = await db.transaction(async (tx) => { + const [order] = await tx + .insert(orders) + .values({ + orderRef: parsed.data.orderRef, + status: 'new', + updatedAt: new Date() + }) + .returning(); + await tx.insert(orderItems).values(parsed.data.items.map((it) => ({ + orderId: order.id, + amazonSku: it.amazonSku, + quantity: it.quantity, + title: it.title ?? null + }))); + return order; + }); + return res.status(201).json({ id: created.id }); + } + catch (err) { + // eslint-disable-next-line no-console + console.error('POST /admin/orders failed', err); + return res.status(500).json({ message: 'internal_error' }); + } +}); +app.get('/admin/orders/:id', async (req, res) => { + const { id } = req.params; + try { + const db = getDb(); + const [order] = await db.select().from(orders).where(eq(orders.id, id)); + if (!order) { + return res.status(404).json({ message: 'not_found' }); + } + const items = await db + .select() + .from(orderItems) + .where(and(eq(orderItems.orderId, id))) + .orderBy(orderItems.createdAt); + const skuList = Array.from(new Set(items.map((it) => it.amazonSku))); + const mappings = skuList.length + ? await db + .select({ amazonSku: skuMappings.amazonSku, expectedFotaModel: skuMappings.expectedFotaModel }) + .from(skuMappings) + .where(inArray(skuMappings.amazonSku, skuList)) + : []; + const expectedModelBySku = new Map(mappings.map((m) => [m.amazonSku, m.expectedFotaModel ?? null])); + const expectedFotaModels = Array.from(new Set(mappings.map((m) => (m.expectedFotaModel ?? '').trim()).filter(Boolean))); + const imeis = await db + .select() + .from(orderImeis) + .where(and(eq(orderImeis.orderId, id))) + .orderBy(orderImeis.createdAt); + return res.json({ + ...order, + items: items.map((it) => ({ + ...it, + expectedFotaModel: expectedModelBySku.get(it.amazonSku) ?? null + })), + expectedFotaModels, + imeis + }); + } + catch (err) { + // eslint-disable-next-line no-console + console.error('GET /admin/orders/:id failed', err); + return res.status(500).json({ message: 'internal_error' }); + } +}); +app.post('/admin/orders/:id/scan-imei', async (req, res) => { + const { id } = req.params; + const parsed = scanImeiSchema.safeParse(req.body); + if (!parsed.success) { + return res.status(400).json({ + message: 'invalid_body', + issues: parsed.error.issues + }); + } + const identifier = parsed.data.imei.trim(); + try { + const db = getDb(); + const [order] = await db.select({ id: orders.id }).from(orders).where(eq(orders.id, id)); + if (!order) { + return res.status(404).json({ message: 'not_found' }); + } + let resolved; + try { + resolved = await resolveTeltonikaDeviceByIdentifier(identifier); + } + catch (err) { + const error = err; + const message = error?.message ? String(error.message) : 'unknown_error'; + const status = error?.response?.status; + const data = error?.response?.data; + console.error('resolveTeltonikaDeviceByIdentifier failed', { + message, + status, + data, + identifier + }); + if (message.includes('TELTONIKA_FOTA_API_TOKEN is required')) { + return res.status(500).json({ message: 'missing_teltonika_token' }); + } + if (message.includes('TELTONIKA_FOTA_USER_AGENT is required')) { + return res.status(500).json({ message: 'missing_teltonika_user_agent' }); + } + if (message === 'invalid_identifier' || message === 'invalid_imei') { + return res.status(400).json({ message }); + } + if (message === 'device_not_found') { + return res.status(404).json({ message }); + } + if (typeof status === 'number') { + return res.status(502).json({ + message: 'teltonika_error', + status, + data + }); + } + return res.status(500).json({ message: 'internal_error', detail: message }); + } + const { imei } = resolved; + let device = resolved.device; + const needsDeviceHydration = !device || + typeof device !== 'object' || + typeof device.model === 'undefined' || + typeof device.serial === 'undefined' || + typeof device.current_firmware === 'undefined' || + typeof device.activity_status === 'undefined'; + if (needsDeviceHydration) { + try { + device = await lookupTeltonikaDeviceByImei(imei); + } + catch (err) { + const error = err; + const message = error?.message ? String(error.message) : 'unknown_error'; + const status = error?.response?.status; + const data = error?.response?.data; + console.error('lookupTeltonikaDeviceByImei failed after resolve', { + message, + status, + data, + imei, + identifier + }); + } + } + try { + const [created] = await db + .insert(orderImeis) + .values({ + orderId: id, + imei + }) + .returning(); + try { + const activityStatus = normalizeFotaActivityStatus(device?.activity_status); + await db + .update(orderImeis) + .set({ + fotaModel: device?.model ?? null, + fotaSerial: device?.serial ?? null, + fotaCurrentFirmware: device?.current_firmware ?? null, + fotaActivityStatus: activityStatus, + fotaSeenAt: device?.seen_at ?? null, + fotaCompanyId: device?.company_id ?? null, + fotaCompanyName: device?.company_name ?? null, + fotaGroupId: device?.group_id ?? null, + fotaGroupName: device?.group_name ?? null, + fotaLookupError: null, + fotaLastLookupAt: new Date() + }) + .where(eq(orderImeis.id, created.id)); + } + catch (err) { + const rawMsg = err instanceof Error ? err.message : String(err); + const msg = rawMsg.includes('Failed query:') ? 'fota_lookup_failed' : rawMsg; + try { + await db + .update(orderImeis) + .set({ + fotaLookupError: msg, + fotaLastLookupAt: new Date() + }) + .where(eq(orderImeis.id, created.id)); + } + catch (writeErr) { + console.error('Failed to persist fotaLookupError', writeErr); + } + } + // Relire l'IMEI depuis la DB pour inclure les données FOTA mises à jour + const [fresh] = await db.select().from(orderImeis).where(eq(orderImeis.id, created.id)); + return res.status(201).json(fresh ?? created); + } + catch (err) { + const maybePgCode = err?.code; + if (maybePgCode === '23505') { + return res.status(409).json({ message: 'already_scanned' }); + } + const error = err; + const message = error?.message ? String(error.message) : 'unknown_error'; + console.error('Insert order imei failed', { + message, + code: error?.code + }); + return res.status(500).json({ message: 'internal_error', detail: message }); + } + } + catch (err) { + const error = err; + const message = error?.message ? String(error.message) : 'unknown_error'; + const status = error?.response?.status; + const data = error?.response?.data; + console.error('POST /admin/orders/:id/scan-imei failed', { + message, + status, + data + }); + if (typeof status === 'number') { + return res.status(502).json({ + message: 'teltonika_error', + status, + data + }); + } + return res.status(500).json({ message: 'internal_error', detail: message }); + } +}); +app.post('/admin/orders/:id/fota-move', async (req, res) => { + const { id } = req.params; + const targetCompanyId = toMaybeInt64(process.env.TELTONIKA_FOTA_TARGET_COMPANY_ID ?? ''); + const targetGroupId = toMaybeInt64(process.env.TELTONIKA_FOTA_TARGET_GROUP_ID ?? ''); + if (targetCompanyId === null || targetGroupId === null) { + return res.status(500).json({ message: 'missing_fota_target_ids' }); + } + try { + const db = getDb(); + const [order] = await db.select({ id: orders.id }).from(orders).where(eq(orders.id, id)); + if (!order) { + return res.status(404).json({ message: 'not_found' }); + } + const items = await db + .select({ amazonSku: orderItems.amazonSku, quantity: orderItems.quantity }) + .from(orderItems) + .where(eq(orderItems.orderId, id)); + const totalExpected = items.reduce((sum, it) => sum + it.quantity, 0); + const skuList = Array.from(new Set(items.map((it) => it.amazonSku))); + const mappings = skuList.length + ? await db + .select({ amazonSku: skuMappings.amazonSku, expectedFotaModel: skuMappings.expectedFotaModel }) + .from(skuMappings) + .where(inArray(skuMappings.amazonSku, skuList)) + : []; + const expectedModels = new Set(mappings.map((m) => (m.expectedFotaModel ?? '').trim()).filter(Boolean)); + const imeis = await db + .select({ + id: orderImeis.id, + imei: orderImeis.imei, + fotaModel: orderImeis.fotaModel, + fotaLookupError: orderImeis.fotaLookupError + }) + .from(orderImeis) + .where(eq(orderImeis.orderId, id)) + .orderBy(orderImeis.createdAt); + if (imeis.length === 0) { + return res.status(400).json({ message: 'no_imeis' }); + } + if (imeis.length !== totalExpected) { + return res.status(400).json({ + message: 'imei_count_mismatch', + scanned: imeis.length, + expected: totalExpected + }); + } + const missingFotaInfo = imeis + .filter((it) => { + const model = (it.fotaModel ?? '').trim(); + if (!model) + return true; + if (it.fotaLookupError) + return true; + return false; + }) + .map((it) => ({ + imei: it.imei, + fotaModel: it.fotaModel ?? null, + fotaLookupError: it.fotaLookupError ?? null + })); + if (missingFotaInfo.length > 0) { + return res.status(400).json({ + message: 'missing_fota_info', + missing: missingFotaInfo + }); + } + if (expectedModels.size > 0) { + const mismatches = imeis + .filter((it) => { + const model = (it.fotaModel ?? '').trim(); + return !expectedModels.has(model); + }) + .map((it) => ({ + imei: it.imei, + fotaModel: it.fotaModel ?? null + })); + if (mismatches.length > 0) { + return res.status(400).json({ + message: 'fota_model_mismatch', + expectedModels: Array.from(expectedModels), + mismatches + }); + } + } + const idList = imeis + .map((x) => toMaybeInt64(x.imei)) + .filter((x) => x !== null); + if (idList.length === 0) { + return res.status(400).json({ message: 'invalid_imeis' }); + } + const result = await teltonikaClient.post('/devices/bulkUpdate', { + source: 'id_list', + id_list: idList, + data: { + company_id: targetCompanyId, + group_id: targetGroupId + } + }, { + headers: getTeltonikaAuthHeaders() + }); + await db + .update(orderImeis) + .set({ + fotaMoveError: null, + fotaMovedAt: new Date() + }) + .where(eq(orderImeis.orderId, id)); + await db + .update(orders) + .set({ + status: 'ready', + updatedAt: new Date() + }) + .where(eq(orders.id, id)); + return res.json({ + ...result.data, + targetCompanyId, + targetGroupId, + movedImeisCount: idList.length, + orderStatus: 'ready' + }); + } + catch (err) { + const msg = err instanceof Error ? err.message : String(err); + console.error('POST /admin/orders/:id/fota-move failed', err); + try { + const db = getDb(); + await db + .update(orderImeis) + .set({ + fotaMoveError: msg + }) + .where(eq(orderImeis.orderId, id)); + } + catch { + // ignore + } + return res.status(500).json({ message: 'internal_error' }); + } +}); +// --- Axonaut --- +const getAxonautApiKey = async () => { + const db = getDb(); + const [row] = await db + .select({ value: apiKeys.value }) + .from(apiKeys) + .where(and(eq(apiKeys.provider, 'axonaut'), eq(apiKeys.label, 'api_key'))); + if (!row) { + throw new Error('axonaut_api_key_not_configured'); + } + return row.value; +}; +const axonautClient = axios.create({ + baseURL: 'https://axonaut.com/api/v2', + timeout: 15000, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + } +}); +app.get('/admin/axonaut/products', async (req, res) => { + try { + const apiKey = await getAxonautApiKey(); + const params = {}; + if (req.query.product_code) + params.product_code = String(req.query.product_code); + if (req.query.internal_id) + params.internal_id = String(req.query.internal_id); + if (req.query.name) + params.name = String(req.query.name); + const result = await axonautClient.get('/products', { + headers: { userApiKey: apiKey }, + params + }); + return res.json(result.data); + } + catch (err) { + const error = err; + const message = error?.message ? String(error.message) : 'unknown_error'; + const status = error?.response?.status; + const data = error?.response?.data; + console.error('GET /admin/axonaut/products failed', { message, status, data }); + if (message.includes('axonaut_api_key_not_configured')) { + return res.status(500).json({ message: 'axonaut_api_key_not_configured' }); + } + if (typeof status === 'number') { + return res.status(502).json({ message: 'axonaut_error', status, data }); + } + return res.status(500).json({ message: 'internal_error', detail: message }); + } +}); +app.post('/admin/orders/:id/axonaut-destock', async (req, res) => { + const { id } = req.params; + try { + const db = getDb(); + const [order] = await db.select().from(orders).where(eq(orders.id, id)); + if (!order) { + return res.status(404).json({ message: 'not_found' }); + } + if (order.axonautDestockedAt) { + return res.status(409).json({ message: 'already_destocked', destockedAt: order.axonautDestockedAt }); + } + let apiKey; + try { + apiKey = await getAxonautApiKey(); + } + catch { + return res.status(500).json({ message: 'axonaut_api_key_not_configured' }); + } + const items = await db + .select({ amazonSku: orderItems.amazonSku, quantity: orderItems.quantity, title: orderItems.title }) + .from(orderItems) + .where(eq(orderItems.orderId, id)); + const skuList = Array.from(new Set(items.map((it) => it.amazonSku))); + const mappings = skuList.length + ? await db + .select({ + amazonSku: skuMappings.amazonSku, + axonautProductInternalId: skuMappings.axonautProductInternalId, + enterpriseSku: skuMappings.enterpriseSku + }) + .from(skuMappings) + .where(inArray(skuMappings.amazonSku, skuList)) + : []; + const mappingBySku = new Map(mappings.map((m) => [m.amazonSku, m])); + const missingMappings = skuList.filter((sku) => { + const m = mappingBySku.get(sku); + return !m || !m.axonautProductInternalId.trim(); + }); + if (missingMappings.length > 0) { + return res.status(400).json({ + message: 'missing_axonaut_product_id', + skus: missingMappings + }); + } + const imeis = await db + .select({ imei: orderImeis.imei, fotaModel: orderImeis.fotaModel }) + .from(orderImeis) + .where(eq(orderImeis.orderId, id)) + .orderBy(orderImeis.createdAt); + const axonautHeaders = { userApiKey: apiKey }; + // Regrouper les quantités par axonaut product internal_id + const qtyByAxonautId = new Map(); + for (const item of items) { + const mapping = mappingBySku.get(item.amazonSku); + if (!mapping) + continue; + const axonautId = mapping.axonautProductInternalId.trim(); + const existing = qtyByAxonautId.get(axonautId); + if (existing) { + existing.quantity += item.quantity; + existing.skus.push(item.amazonSku); + } + else { + qtyByAxonautId.set(axonautId, { quantity: item.quantity, skus: [item.amazonSku] }); + } + } + const stockResults = []; + // Destocker chaque produit Axonaut + for (const [axonautInternalId, { quantity, skus }] of qtyByAxonautId) { + const productId = axonautInternalId; + // Vérifier que le produit existe + try { + const productRes = await axonautClient.get(`/products/${productId}`, { headers: axonautHeaders }); + console.log(`[axonaut] GET /products/${productId} → ${productRes.status}`); + } + catch (checkErr) { + console.error(`[axonaut] GET /products/${productId} FAILED`, checkErr?.response?.status, checkErr?.response?.data); + if (checkErr?.response?.status === 404) { + await db + .update(orders) + .set({ axonautDestockError: `Produit Axonaut introuvable: id=${productId}`, updatedAt: new Date() }) + .where(eq(orders.id, id)); + return res.status(400).json({ + message: 'axonaut_product_not_found', + axonautInternalId + }); + } + throw checkErr; + } + // Récupérer le stock actuel + const stockRes = await axonautClient.get(`/products/${productId}/stock`, { + headers: axonautHeaders + }); + const stockData = stockRes.data; + console.log(`[axonaut] GET /products/${productId}/stock → `, JSON.stringify(stockData)); + // Supporter les deux noms de champ possibles + let currentStock; + if (typeof stockData?.current_stock === 'number') { + currentStock = stockData.current_stock; + } + else if (typeof stockData?.quantity === 'number') { + currentStock = stockData.quantity; + } + else if (typeof stockData?.stock === 'number') { + currentStock = stockData.stock; + } + else { + // Fallback : chercher n'importe quel champ numérique qui ressemble à un stock + console.error(`[axonaut] Impossible de déterminer le stock actuel. Données brutes:`, stockData); + await db + .update(orders) + .set({ axonautDestockError: `Stock Axonaut illisible pour produit ${productId}: ${JSON.stringify(stockData)}`, updatedAt: new Date() }) + .where(eq(orders.id, id)); + return res.status(400).json({ + message: 'axonaut_stock_unreadable', + axonautInternalId, + rawStockData: stockData + }); + } + const newStock = currentStock - quantity; + // Mettre à jour le stock (valeur absolue) — Axonaut attend "stock" dans le PATCH + const patchBody = { stock: newStock }; + console.log(`[axonaut] PATCH /products/${productId}/stock body:`, JSON.stringify(patchBody)); + try { + const patchRes = await axonautClient.patch(`/products/${productId}/stock`, patchBody, { + headers: axonautHeaders + }); + console.log(`[axonaut] PATCH /products/${productId}/stock → ${patchRes.status}`, JSON.stringify(patchRes.data)); + } + catch (patchErr) { + console.error(`[axonaut] PATCH /products/${productId}/stock FAILED`, patchErr?.response?.status, JSON.stringify(patchErr?.response?.data)); + throw patchErr; + } + stockResults.push({ + axonautInternalId, + skus, + previousStock: currentStock, + newStock, + destocked: quantity + }); + } + // Créer la note avec les IMEI (non-bloquant : si ça échoue, le destockage reste valide) + let noteCreated = false; + try { + const imeiLines = imeis.map((it) => { + const model = it.fotaModel ? ` (${it.fotaModel})` : ''; + return `- ${it.imei}${model}`; + }).join('\n'); + const stockSummary = stockResults.map((r) => { + return `${r.skus.join(', ')}: ${r.previousStock} → ${r.newStock} (-${r.destocked})`; + }).join('\n'); + const noteContent = [ + `Destockage commande Amazon #${order.orderRef}`, + '', + 'Stock modifié :', + stockSummary, + '', + `IMEI (${imeis.length}) :`, + imeiLines + ].join('\n'); + // company_id : body override > société Amazon par défaut + const AXONAUT_AMAZON_COMPANY_ID = 45114290; + const companyId = req.body?.axonaut_company_id + ? Number(req.body.axonaut_company_id) + : AXONAUT_AMAZON_COMPANY_ID; + const eventPayload = { + company_id: companyId, + title: `Vente Amazon - ${order.orderRef}`, + content: noteContent, + nature: 6, + date: new Date().toISOString(), + is_done: true + }; + console.log(`[axonaut] POST /events body:`, JSON.stringify(eventPayload)); + const eventRes = await axonautClient.post('/events', eventPayload, { + headers: axonautHeaders + }); + console.log(`[axonaut] POST /events → ${eventRes.status}`); + noteCreated = true; + } + catch (eventErr) { + console.error(`[axonaut] POST /events FAILED (non-bloquant)`, eventErr?.response?.status, JSON.stringify(eventErr?.response?.data)); + } + // Marquer la commande comme destockée + await db + .update(orders) + .set({ + axonautDestockedAt: new Date(), + axonautDestockError: null, + updatedAt: new Date() + }) + .where(eq(orders.id, id)); + return res.json({ + message: 'destocked', + stockResults, + imeiCount: imeis.length, + noteCreated + }); + } + catch (err) { + const error = err; + const message = error?.message ? String(error.message) : 'unknown_error'; + const status = error?.response?.status; + const data = error?.response?.data; + console.error('POST /admin/orders/:id/axonaut-destock failed', { + message, + status, + data + }); + try { + const db = getDb(); + await db + .update(orders) + .set({ axonautDestockError: message, updatedAt: new Date() }) + .where(eq(orders.id, id)); + } + catch { + // ignore + } + if (typeof status === 'number') { + return res.status(502).json({ + message: 'axonaut_error', + status, + data + }); + } + return res.status(500).json({ message: 'internal_error', detail: message }); + } +}); +app.delete('/admin/orders/:orderId/imeis/:imeiId', async (req, res) => { + const { orderId, imeiId } = req.params; + try { + const db = getDb(); + const deleted = await db + .delete(orderImeis) + .where(and(eq(orderImeis.id, imeiId), eq(orderImeis.orderId, orderId))) + .returning({ id: orderImeis.id }); + if (deleted.length === 0) { + return res.status(404).json({ message: 'not_found' }); + } + return res.status(204).send(); + } + catch (err) { + // eslint-disable-next-line no-console + console.error('DELETE /admin/orders/:orderId/imeis/:imeiId failed', err); + return res.status(500).json({ message: 'internal_error' }); + } +}); +// --- API Keys --- +const maskValue = (value) => { + if (value.length <= 4) + return '****'; + return '****' + value.slice(-4); +}; +app.get('/admin/api-keys', async (_req, res) => { + try { + const db = getDb(); + const rows = await db.select().from(apiKeys).orderBy(apiKeys.provider, apiKeys.label); + const masked = rows.map((row) => ({ + ...row, + value: maskValue(row.value) + })); + res.json(masked); + } + catch (err) { + console.error('GET /admin/api-keys failed', err); + res.status(500).json({ message: 'internal_error' }); + } +}); +app.post('/admin/api-keys', async (req, res) => { + const parsed = apiKeyCreateSchema.safeParse(req.body); + if (!parsed.success) { + return res.status(400).json({ + message: 'invalid_body', + issues: parsed.error.issues + }); + } + try { + const db = getDb(); + const [created] = await db + .insert(apiKeys) + .values({ + provider: parsed.data.provider, + label: parsed.data.label, + value: parsed.data.value + }) + .returning(); + return res.status(201).json({ + ...created, + value: maskValue(created.value) + }); + } + catch (err) { + const maybePgCode = err?.code; + if (maybePgCode === '23505') { + return res.status(409).json({ message: 'duplicate_key' }); + } + console.error('POST /admin/api-keys failed', err); + return res.status(500).json({ message: 'internal_error' }); + } +}); +app.patch('/admin/api-keys/:id', async (req, res) => { + const { id } = req.params; + const parsed = apiKeyUpdateSchema.safeParse(req.body); + if (!parsed.success) { + return res.status(400).json({ + message: 'invalid_body', + issues: parsed.error.issues + }); + } + try { + const db = getDb(); + const [updated] = await db + .update(apiKeys) + .set({ + value: parsed.data.value, + updatedAt: new Date() + }) + .where(eq(apiKeys.id, id)) + .returning(); + if (!updated) { + return res.status(404).json({ message: 'not_found' }); + } + return res.json({ + ...updated, + value: maskValue(updated.value) + }); + } + catch (err) { + console.error('PATCH /admin/api-keys failed', err); + return res.status(500).json({ message: 'internal_error' }); + } +}); +app.delete('/admin/api-keys/:id', async (req, res) => { + const { id } = req.params; + try { + const db = getDb(); + const deleted = await db + .delete(apiKeys) + .where(eq(apiKeys.id, id)) + .returning({ id: apiKeys.id }); + if (deleted.length === 0) { + return res.status(404).json({ message: 'not_found' }); + } + return res.status(204).send(); + } + catch (err) { + console.error('DELETE /admin/api-keys failed', err); + return res.status(500).json({ message: 'internal_error' }); + } +}); +// --- Users --- +app.get('/admin/users', async (_req, res) => { + try { + const db = getDb(); + const rows = await db + .select({ + id: users.id, + username: users.username, + displayName: users.displayName, + createdAt: users.createdAt + }) + .from(users) + .orderBy(users.createdAt); + return res.json(rows); + } + catch (err) { + console.error('GET /admin/users failed', err); + return res.status(500).json({ message: 'internal_error' }); + } +}); +app.post('/admin/users', async (req, res) => { + const parsed = registerSchema.safeParse(req.body); + if (!parsed.success) { + return res.status(400).json({ message: 'invalid_body', issues: parsed.error.issues }); + } + try { + const db = getDb(); + const passwordHash = await bcrypt.hash(parsed.data.password, 12); + const [created] = await db + .insert(users) + .values({ + username: parsed.data.username, + passwordHash, + displayName: parsed.data.displayName + }) + .returning({ + id: users.id, + username: users.username, + displayName: users.displayName, + createdAt: users.createdAt + }); + return res.status(201).json(created); + } + catch (err) { + const maybePgCode = err?.code; + if (maybePgCode === '23505') { + return res.status(409).json({ message: 'username_taken' }); + } + console.error('POST /admin/users failed', err); + return res.status(500).json({ message: 'internal_error' }); + } +}); +app.patch('/admin/users/:id/password', async (req, res) => { + const { id } = req.params; + const parsed = changePasswordSchema.safeParse(req.body); + if (!parsed.success) { + return res.status(400).json({ message: 'invalid_body', issues: parsed.error.issues }); + } + try { + const db = getDb(); + const passwordHash = await bcrypt.hash(parsed.data.password, 12); + const [updated] = await db + .update(users) + .set({ passwordHash, updatedAt: new Date() }) + .where(eq(users.id, id)) + .returning({ id: users.id }); + if (!updated) { + return res.status(404).json({ message: 'not_found' }); + } + return res.json({ message: 'password_changed' }); + } + catch (err) { + console.error('PATCH /admin/users/:id/password failed', err); + return res.status(500).json({ message: 'internal_error' }); + } +}); +app.delete('/admin/users/:id', async (req, res) => { + const { id } = req.params; + if (req.user?.id === id) { + return res.status(400).json({ message: 'cannot_delete_self' }); + } + try { + const db = getDb(); + const deleted = await db + .delete(users) + .where(eq(users.id, id)) + .returning({ id: users.id }); + if (deleted.length === 0) { + return res.status(404).json({ message: 'not_found' }); + } + return res.status(204).send(); + } + catch (err) { + console.error('DELETE /admin/users/:id failed', err); + return res.status(500).json({ message: 'internal_error' }); + } +}); +// --- Amazon SP-API --- +const getAmazonCredentials = async () => { + const db = getDb(); + const rows = await db + .select({ label: apiKeys.label, value: apiKeys.value }) + .from(apiKeys) + .where(eq(apiKeys.provider, 'amazon')); + const map = new Map(rows.map((r) => [r.label, r.value])); + const clientId = map.get('client_id'); + const clientSecret = map.get('client_secret'); + const refreshToken = map.get('refresh_token'); + if (!clientId || !clientSecret || !refreshToken) { + const missing = ['client_id', 'client_secret', 'refresh_token'].filter((k) => !map.get(k)); + throw new Error(`amazon_credentials_missing: ${missing.join(', ')}`); + } + return { clientId, clientSecret, refreshToken }; +}; +const AMAZON_MARKETPLACE_FR = 'A13V1IB3VIYZZH'; +const createAmazonClient = async () => { + const { clientId, clientSecret, refreshToken } = await getAmazonCredentials(); + const { SellingPartner } = await import('amazon-sp-api'); + return new SellingPartner({ + region: 'eu', + refresh_token: refreshToken, + credentials: { + SELLING_PARTNER_APP_CLIENT_ID: clientId, + SELLING_PARTNER_APP_CLIENT_SECRET: clientSecret + }, + options: { + auto_request_throttled: true + } + }); +}; +// --- Amazon shipping address via RDT --- +const splitAmazonName = (name) => { + const parts = name.trim().split(/\s+/); + if (parts.length <= 1) { + return { firstName: '', lastName: parts[0] ?? '' }; + } + return { firstName: parts[0], lastName: parts.slice(1).join(' ') }; +}; +const fetchAmazonShippingAddress = async (sp, amazonOrderId) => { + try { + const rdtRes = await sp.callAPI({ + operation: 'createRestrictedDataToken', + endpoint: 'tokens', + body: { + restrictedResources: [{ + method: 'GET', + path: `/orders/v0/orders/${amazonOrderId}/address`, + dataElements: ['shippingAddress'] + }] + } + }); + const rdt = rdtRes?.restrictedDataToken ?? rdtRes?.payload?.restrictedDataToken; + if (!rdt) { + console.error(`[amazon-address] No RDT returned for ${amazonOrderId}`); + return null; + } + const addrRes = await sp.callAPI({ + operation: 'getOrderAddress', + endpoint: 'orders', + path: { orderId: amazonOrderId }, + restricted_data_token: rdt + }); + const address = addrRes?.payload?.ShippingAddress ?? addrRes?.ShippingAddress; + return address ?? null; + } + catch (err) { + const msg = err instanceof Error ? err.message : String(err); + console.error(`[amazon-address] Failed for ${amazonOrderId}:`, msg); + return null; + } +}; +const fetchAndStoreShippingAddress = async (sp, dbOrderId, amazonOrderId) => { + try { + const address = await fetchAmazonShippingAddress(sp, amazonOrderId); + const db = getDb(); + if (!address || !address.Name) { + await db + .update(orders) + .set({ shippingFetchError: 'no_address_returned', updatedAt: new Date() }) + .where(eq(orders.id, dbOrderId)); + return false; + } + const { firstName, lastName } = splitAmazonName(address.Name); + await db + .update(orders) + .set({ + shippingName: address.Name, + shippingFirstName: firstName, + shippingLastName: lastName, + shippingLine1: address.AddressLine1 ?? null, + shippingLine2: address.AddressLine2 ?? null, + shippingLine3: address.AddressLine3 ?? null, + shippingCity: address.City ?? null, + shippingZipCode: address.PostalCode ?? null, + shippingCountryCode: address.CountryCode ?? address.Country ?? null, + shippingState: address.StateOrRegion ?? null, + shippingPhone: address.Phone ?? null, + shippingAddressType: address.AddressType ?? null, + shippingFetchedAt: new Date(), + shippingFetchError: null, + updatedAt: new Date() + }) + .where(eq(orders.id, dbOrderId)); + return true; + } + catch (err) { + const msg = err instanceof Error ? err.message : String(err); + console.error(`[amazon-address] Store failed for order ${dbOrderId}:`, msg); + try { + const db = getDb(); + await db + .update(orders) + .set({ shippingFetchError: msg, updatedAt: new Date() }) + .where(eq(orders.id, dbOrderId)); + } + catch { + // ignore + } + return false; + } +}; +// Test de connexion Amazon SP-API +app.get('/admin/amazon/test', async (req, res) => { + try { + // Étape 1 : vérifier les credentials + const { clientId, clientSecret, refreshToken } = await getAmazonCredentials(); + // Étape 2 : tester le token LWA (obtenir un access_token) + let accessTokenOk = false; + let lwaError = null; + try { + const tokenRes = await axios.post('https://api.amazon.com/auth/o2/token', new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refreshToken, + client_id: clientId, + client_secret: clientSecret + }).toString(), { + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + timeout: 10000 + }); + accessTokenOk = !!tokenRes.data?.access_token; + } + catch (err) { + const e = err; + lwaError = e?.response?.data?.error_description ?? e?.response?.data?.error ?? e?.message ?? 'unknown'; + console.error('LWA token test failed', { status: e?.response?.status, data: e?.response?.data }); + } + if (!accessTokenOk) { + return res.status(401).json({ + connected: false, + step: 'lwa_token', + message: 'lwa_authentication_failed', + detail: lwaError + }); + } + // Étape 3 : tester l'appel SP-API + const sp = await createAmazonClient(); + const response = await sp.callAPI({ + operation: 'getOrders', + endpoint: 'orders', + query: { + MarketplaceIds: [AMAZON_MARKETPLACE_FR], + CreatedAfter: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString() + } + }); + const orderCount = (response?.Orders ?? response?.payload?.Orders ?? []).length; + return res.json({ + connected: true, + marketplace: 'Amazon.fr (A13V1IB3VIYZZH)', + ordersLast24h: orderCount + }); + } + catch (err) { + const error = err; + const message = error?.message ? String(error.message) : 'unknown_error'; + const code = error?.code ?? error?.statusCode; + const body = error?.response?.data ?? error?.details; + console.error('GET /admin/amazon/test failed', { message, code, body, keys: Object.keys(error ?? {}) }); + if (message.includes('amazon_credentials_missing')) { + return res.status(500).json({ connected: false, step: 'credentials', message }); + } + return res.status(502).json({ + connected: false, + step: 'sp_api_call', + message: 'amazon_api_error', + detail: message, + code, + body + }); + } +}); +// Liste les commandes Amazon (Unshipped par défaut) +app.get('/admin/amazon/orders', async (req, res) => { + try { + const sp = await createAmazonClient(); + const daysBack = Number(req.query.days) || 7; + const createdAfter = new Date(Date.now() - daysBack * 24 * 60 * 60 * 1000).toISOString(); + const statusParam = req.query.statuses; + const orderStatuses = typeof statusParam === 'string' && statusParam.trim() + ? statusParam.split(',').map((s) => s.trim()) + : ['Unshipped', 'PartiallyShipped']; + const response = await sp.callAPI({ + operation: 'getOrders', + endpoint: 'orders', + query: { + MarketplaceIds: [AMAZON_MARKETPLACE_FR], + CreatedAfter: createdAfter, + OrderStatuses: orderStatuses + } + }); + const amazonOrders = response?.Orders ?? response?.payload?.Orders ?? []; + return res.json({ orders: amazonOrders, count: amazonOrders.length }); + } + catch (err) { + const error = err; + const message = error?.message ? String(error.message) : 'unknown_error'; + console.error('GET /admin/amazon/orders failed', { message, status: error?.statusCode, details: error?.details }); + if (message.includes('amazon_credentials_missing')) { + return res.status(500).json({ message }); + } + return res.status(502).json({ message: 'amazon_api_error', detail: message }); + } +}); +// Récupère les articles d'une commande Amazon +app.get('/admin/amazon/orders/:amazonOrderId/items', async (req, res) => { + try { + const sp = await createAmazonClient(); + const response = await sp.callAPI({ + operation: 'getOrderItems', + endpoint: 'orders', + path: { orderId: req.params.amazonOrderId } + }); + const items = response?.OrderItems ?? response?.payload?.OrderItems ?? []; + return res.json({ items }); + } + catch (err) { + const error = err; + const message = error?.message ? String(error.message) : 'unknown_error'; + console.error('GET /admin/amazon/orders/:id/items failed', { message }); + return res.status(502).json({ message: 'amazon_api_error', detail: message }); + } +}); +// Importe une commande Amazon dans notre base +app.post('/admin/amazon/import', async (req, res) => { + const { amazonOrderId } = req.body ?? {}; + if (!amazonOrderId || typeof amazonOrderId !== 'string') { + return res.status(400).json({ message: 'missing_amazon_order_id' }); + } + try { + const db = getDb(); + // Vérifier si la commande existe déjà + const [existing] = await db + .select({ id: orders.id }) + .from(orders) + .where(eq(orders.orderRef, amazonOrderId)); + if (existing) { + return res.status(409).json({ message: 'order_already_exists', orderId: existing.id }); + } + // Récupérer les articles depuis Amazon + const sp = await createAmazonClient(); + const itemsResponse = await sp.callAPI({ + operation: 'getOrderItems', + endpoint: 'orders', + path: { orderId: amazonOrderId } + }); + const amazonItems = itemsResponse?.OrderItems ?? itemsResponse?.payload?.OrderItems ?? []; + if (amazonItems.length === 0) { + return res.status(400).json({ message: 'no_items_found' }); + } + // Créer la commande + const [newOrder] = await db + .insert(orders) + .values({ + orderRef: amazonOrderId, + status: 'new' + }) + .returning(); + // Créer les articles + const itemValues = amazonItems.map((item) => ({ + orderId: newOrder.id, + amazonSku: item.SellerSKU ?? 'UNKNOWN', + amazonOrderItemId: item.OrderItemId ?? null, + quantity: item.QuantityOrdered ?? 1, + title: item.Title ?? null + })); + await db.insert(orderItems).values(itemValues); + // Récupérer l'adresse de livraison via RDT + let addressFetched = false; + try { + addressFetched = await fetchAndStoreShippingAddress(sp, newOrder.id, amazonOrderId); + } + catch (err) { + console.error(`[amazon-import] Address fetch failed for ${amazonOrderId}:`, err); + } + return res.status(201).json({ + message: 'imported', + orderId: newOrder.id, + orderRef: amazonOrderId, + itemCount: itemValues.length, + addressFetched + }); + } + catch (err) { + const error = err; + const message = error?.message ? String(error.message) : 'unknown_error'; + console.error('POST /admin/amazon/import failed', { message }); + if (message.includes('amazon_credentials_missing')) { + return res.status(500).json({ message }); + } + return res.status(502).json({ message: 'amazon_api_error', detail: message }); + } +}); +// Associer un numéro de suivi Colissimo manuellement +app.post('/admin/orders/:id/tracking', async (req, res) => { + const { id } = req.params; + const { trackingNumber } = req.body ?? {}; + if (!trackingNumber || typeof trackingNumber !== 'string' || !trackingNumber.trim()) { + return res.status(400).json({ message: 'missing_tracking_number' }); + } + try { + const db = getDb(); + const [order] = await db.select({ id: orders.id }).from(orders).where(eq(orders.id, id)); + if (!order) { + return res.status(404).json({ message: 'not_found' }); + } + await db + .update(orders) + .set({ + colissimoTrackingNumber: trackingNumber.trim(), + colissimoShippedAt: new Date(), + colissimoError: null, + status: 'shipped', + updatedAt: new Date() + }) + .where(eq(orders.id, id)); + return res.json({ message: 'tracking_saved', trackingNumber: trackingNumber.trim() }); + } + catch (err) { + const error = err; + const message = error?.message ? String(error.message) : 'unknown_error'; + console.error('POST /admin/orders/:id/tracking failed', { message }); + return res.status(500).json({ message: 'internal_error', detail: message }); + } +}); +app.delete('/admin/orders/:id/tracking', async (req, res) => { + const { id } = req.params; + try { + const db = getDb(); + const [order] = await db + .select({ + id: orders.id, + colissimoTrackingNumber: orders.colissimoTrackingNumber, + amazonTrackingConfirmedAt: orders.amazonTrackingConfirmedAt + }) + .from(orders) + .where(eq(orders.id, id)); + if (!order) { + return res.status(404).json({ message: 'not_found' }); + } + if (!order.colissimoTrackingNumber) { + return res.status(400).json({ message: 'no_tracking_to_delete' }); + } + if (order.amazonTrackingConfirmedAt) { + return res.status(409).json({ message: 'tracking_already_confirmed_on_amazon' }); + } + await db + .update(orders) + .set({ + colissimoTrackingNumber: null, + colissimoShippedAt: null, + colissimoLabelUrl: null, + colissimoError: null, + status: 'ready', + updatedAt: new Date() + }) + .where(eq(orders.id, id)); + return res.json({ message: 'tracking_deleted' }); + } + catch (err) { + const error = err; + const message = error?.message ? String(error.message) : 'unknown_error'; + console.error('DELETE /admin/orders/:id/tracking failed', { message }); + return res.status(500).json({ message: 'internal_error', detail: message }); + } +}); +// Récupérer manuellement l'adresse Amazon pour une commande +app.post('/admin/orders/:id/fetch-address', async (req, res) => { + const { id } = req.params; + try { + const db = getDb(); + const [order] = await db.select({ id: orders.id, orderRef: orders.orderRef }).from(orders).where(eq(orders.id, id)); + if (!order) { + return res.status(404).json({ message: 'not_found' }); + } + let sp; + try { + sp = await createAmazonClient(); + } + catch (err) { + const msg = err instanceof Error ? err.message : String(err); + return res.status(500).json({ message: msg }); + } + const success = await fetchAndStoreShippingAddress(sp, order.id, order.orderRef); + if (!success) { + return res.status(502).json({ message: 'address_fetch_failed' }); + } + // Relire la commande pour renvoyer l'adresse + const [updated] = await db.select().from(orders).where(eq(orders.id, id)); + return res.json({ + message: 'address_fetched', + shippingName: updated?.shippingName ?? null, + shippingFirstName: updated?.shippingFirstName ?? null, + shippingLastName: updated?.shippingLastName ?? null, + shippingLine1: updated?.shippingLine1 ?? null, + shippingLine2: updated?.shippingLine2 ?? null, + shippingLine3: updated?.shippingLine3 ?? null, + shippingCity: updated?.shippingCity ?? null, + shippingZipCode: updated?.shippingZipCode ?? null, + shippingCountryCode: updated?.shippingCountryCode ?? null, + shippingPhone: updated?.shippingPhone ?? null + }); + } + catch (err) { + const error = err; + const message = error?.message ? String(error.message) : 'unknown_error'; + console.error('POST /admin/orders/:id/fetch-address failed', { message }); + return res.status(500).json({ message: 'internal_error', detail: message }); + } +}); +// Confirmer l'expédition sur Amazon (transmet le tracking Colissimo) +app.post('/admin/orders/:id/amazon-confirm-shipment', async (req, res) => { + const { id } = req.params; + try { + const db = getDb(); + const [order] = await db.select().from(orders).where(eq(orders.id, id)); + if (!order) { + return res.status(404).json({ message: 'not_found' }); + } + if (!order.colissimoTrackingNumber) { + return res.status(400).json({ message: 'no_tracking_number', detail: 'Générez d\'abord une étiquette Colissimo' }); + } + if (order.amazonTrackingConfirmedAt) { + return res.status(409).json({ + message: 'already_confirmed', + confirmedAt: order.amazonTrackingConfirmedAt + }); + } + // Récupérer les articles avec leurs OrderItemId Amazon + const items = await db + .select({ + amazonOrderItemId: orderItems.amazonOrderItemId, + quantity: orderItems.quantity + }) + .from(orderItems) + .where(eq(orderItems.orderId, id)); + const orderItemsForAmazon = items + .filter((it) => it.amazonOrderItemId) + .map((it) => ({ + orderItemId: it.amazonOrderItemId, + quantity: it.quantity + })); + if (orderItemsForAmazon.length === 0) { + return res.status(400).json({ + message: 'no_amazon_order_item_ids', + detail: 'Les articles n\'ont pas d\'OrderItemId Amazon. Réimportez la commande.' + }); + } + const sp = await createAmazonClient(); + await sp.callAPI({ + operation: 'confirmShipment', + endpoint: 'orders', + path: { orderId: order.orderRef }, + body: { + marketplaceId: AMAZON_MARKETPLACE_FR, + packageDetail: { + packageReferenceId: '1', + carrierCode: 'Colissimo', + carrierName: 'Colissimo', + shippingMethod: 'Standard', + trackingNumber: order.colissimoTrackingNumber, + shipDate: (order.colissimoShippedAt ?? new Date()).toISOString(), + orderItems: orderItemsForAmazon + } + } + }); + await db + .update(orders) + .set({ + amazonTrackingConfirmedAt: new Date(), + amazonTrackingError: null, + status: 'done', + updatedAt: new Date() + }) + .where(eq(orders.id, id)); + return res.json({ message: 'shipment_confirmed', trackingNumber: order.colissimoTrackingNumber }); + } + catch (err) { + const error = err; + const message = error?.message ? String(error.message) : 'unknown_error'; + console.error('POST /admin/orders/:id/amazon-confirm-shipment failed', { message }); + try { + const db = getDb(); + await db + .update(orders) + .set({ amazonTrackingError: message, updatedAt: new Date() }) + .where(eq(orders.id, id)); + } + catch { + // ignore + } + if (message.includes('amazon_credentials_missing')) { + return res.status(500).json({ message }); + } + return res.status(502).json({ message: 'amazon_confirm_error', detail: message }); + } +}); +// --- Colissimo --- +const COLISSIMO_BASE_URL = 'https://ws.colissimo.fr/sls-ws/SlsServiceWSRest/3.1'; +const getColissimoCredentials = async () => { + const db = getDb(); + const rows = await db + .select({ label: apiKeys.label, value: apiKeys.value }) + .from(apiKeys) + .where(eq(apiKeys.provider, 'laposte')); + const map = new Map(rows.map((r) => [r.label, r.value])); + const contractNumber = map.get('contract_number'); + const password = map.get('password'); + if (!contractNumber || !password) { + const missing = ['contract_number', 'password'].filter((k) => !map.get(k)); + throw new Error(`colissimo_credentials_missing: ${missing.join(', ')}`); + } + return { contractNumber, password }; +}; +// Parse une réponse MTOM multipart pour extraire le JSON et le PDF +const parseColissimoMtomResponse = (buffer, contentType) => { + const boundaryMatch = contentType.match(/boundary="?([^";]+)"?/); + if (!boundaryMatch) { + // Peut-être une réponse JSON simple (erreur) + const text = buffer.toString('utf-8'); + try { + return { json: JSON.parse(text), pdf: null }; + } + catch { + throw new Error('colissimo_invalid_response: no boundary and not JSON'); + } + } + const boundary = boundaryMatch[1]; + const raw = buffer.toString('binary'); + const parts = raw.split('--' + boundary).filter((p) => p.trim() && p.trim() !== '--'); + let json = null; + let pdf = null; + for (const part of parts) { + const headerEnd = part.indexOf('\r\n\r\n'); + if (headerEnd === -1) + continue; + const headers = part.substring(0, headerEnd).toLowerCase(); + const body = part.substring(headerEnd + 4); + // Retirer le trailing \r\n + const cleanBody = body.replace(/\r\n$/, ''); + if (headers.includes('application/json')) { + try { + json = JSON.parse(cleanBody); + } + catch { + // ignore + } + } + else if (headers.includes('application/pdf') || headers.includes('application/octet-stream')) { + pdf = Buffer.from(cleanBody, 'binary'); + } + } + return { json, pdf }; +}; +// Adresse expéditeur par défaut (à configurer selon votre entrepôt) +const SENDER_ADDRESS = { + companyName: 'Géolock', + line2: '13 allée des cabanes', + countryCode: 'FR', + city: 'Gujan-Mestras', + zipCode: '33470' +}; +// Génère une étiquette Colissimo pour une commande +app.post('/admin/orders/:id/colissimo-label', async (req, res) => { + const { id } = req.params; + try { + const db = getDb(); + const [order] = await db.select().from(orders).where(eq(orders.id, id)); + if (!order) { + return res.status(404).json({ message: 'not_found' }); + } + if (order.colissimoTrackingNumber) { + return res.status(409).json({ + message: 'label_already_generated', + trackingNumber: order.colissimoTrackingNumber + }); + } + const { contractNumber, password } = await getColissimoCredentials(); + // Données du destinataire : body prioritaire, fallback sur adresse stockée en DB + const body = req.body ?? {}; + const lastName = body.lastName || order.shippingLastName || ''; + const firstName = body.firstName || order.shippingFirstName || ''; + const line2 = body.line2 || order.shippingLine1 || ''; + const line3 = body.line3 || order.shippingLine2 || undefined; + const countryCode = body.countryCode || order.shippingCountryCode || 'FR'; + const city = body.city || order.shippingCity || ''; + const zipCode = body.zipCode || order.shippingZipCode || ''; + const weight = body.weight ?? 0.5; + const productCode = body.productCode ?? 'DOM'; + if (!lastName || !line2 || !city || !zipCode) { + return res.status(400).json({ + message: 'missing_address_fields', + required: ['lastName', 'line2', 'city', 'zipCode'] + }); + } + const depositDate = new Date().toISOString().split('T')[0]; + const colissimoPayload = { + contractNumber, + password, + outputFormat: { + x: 0, + y: 0, + outputPrintingType: 'PDF_10x15_300dpi' + }, + letter: { + service: { + productCode, + depositDate, + orderNumber: order.orderRef, + commercialName: 'Localiztoi' + }, + parcel: { + weight: Number(weight) + }, + sender: { + address: SENDER_ADDRESS + }, + addressee: { + address: { + lastName, + firstName: firstName ?? '', + line2, + ...(line3 ? { line3 } : {}), + countryCode, + city, + zipCode + } + } + } + }; + const response = await axios.post(`${COLISSIMO_BASE_URL}/generateLabel`, colissimoPayload, { + headers: { 'Content-Type': 'application/json', Accept: '*/*' }, + responseType: 'arraybuffer', + timeout: 30000, + validateStatus: () => true + }); + // Si erreur HTTP brute de Colissimo + if (response.status >= 400) { + const bodyText = Buffer.from(response.data).toString('utf-8').substring(0, 2000); + let parsed = null; + try { + parsed = JSON.parse(bodyText); + } + catch { /* ignore */ } + const errorDetail = parsed?.messages?.[0]?.messageContent ?? parsed?.message ?? bodyText; + await db + .update(orders) + .set({ colissimoError: `HTTP ${response.status}: ${errorDetail}`, updatedAt: new Date() }) + .where(eq(orders.id, id)); + return res.status(response.status).json({ + message: 'colissimo_label_error', + httpStatus: response.status, + detail: parsed ?? bodyText + }); + } + const responseContentType = response.headers['content-type'] ?? ''; + const { json: labelJson, pdf } = parseColissimoMtomResponse(Buffer.from(response.data), responseContentType); + // Vérifier les erreurs + const messages = labelJson?.messages ?? []; + const errorMsg = messages.find((m) => m.type === 'ERROR'); + if (errorMsg) { + await db + .update(orders) + .set({ colissimoError: `${errorMsg.id}: ${errorMsg.messageContent ?? errorMsg.id}`, updatedAt: new Date() }) + .where(eq(orders.id, id)); + return res.status(400).json({ + message: 'colissimo_label_error', + error: errorMsg + }); + } + const trackingNumber = labelJson?.labelResponse?.parcelNumber ?? null; + // Sauvegarder le PDF sur le disque + let labelUrl = null; + if (pdf && trackingNumber) { + const fs = await import('fs/promises'); + const path = await import('path'); + const labelsDir = path.join(process.cwd(), 'labels'); + await fs.mkdir(labelsDir, { recursive: true }); + const filename = `${trackingNumber}.pdf`; + await fs.writeFile(path.join(labelsDir, filename), pdf); + labelUrl = `/labels/${filename}`; + } + // Mettre à jour la commande + await db + .update(orders) + .set({ + colissimoTrackingNumber: trackingNumber, + colissimoLabelUrl: labelUrl, + colissimoShippedAt: new Date(), + colissimoError: null, + status: 'shipped', + updatedAt: new Date() + }) + .where(eq(orders.id, id)); + return res.json({ + message: 'label_generated', + trackingNumber, + labelUrl + }); + } + catch (err) { + const error = err; + const message = error?.message ? String(error.message) : 'unknown_error'; + console.error('POST /admin/orders/:id/colissimo-label failed', { message, status: error?.response?.status, data: error?.response?.data?.toString?.()?.substring(0, 500) }); + if (message.includes('colissimo_credentials_missing')) { + return res.status(500).json({ message }); + } + try { + const db = getDb(); + await db + .update(orders) + .set({ colissimoError: message, updatedAt: new Date() }) + .where(eq(orders.id, id)); + } + catch { + // ignore + } + return res.status(502).json({ message: 'colissimo_error', detail: message }); + } +}); +// Télécharger l'étiquette PDF +app.get('/labels/:filename', async (req, res) => { + try { + const fs = await import('fs/promises'); + const path = await import('path'); + // Sécurité : empêcher path traversal + const filename = path.basename(req.params.filename); + if (!filename || filename !== req.params.filename || filename.includes('..')) { + return res.status(400).json({ message: 'invalid_filename' }); + } + const labelsDir = path.resolve(process.cwd(), 'labels'); + const filePath = path.join(labelsDir, filename); + // Vérifier que le chemin résolu est bien dans le dossier labels + if (!filePath.startsWith(labelsDir + path.sep)) { + return res.status(400).json({ message: 'invalid_filename' }); + } + const data = await fs.readFile(filePath); + res.setHeader('Content-Type', 'application/pdf'); + res.setHeader('Content-Disposition', `inline; filename="${filename}"`); + return res.send(data); + } + catch { + return res.status(404).json({ message: 'label_not_found' }); + } +}); +// Test connexion Colissimo +app.get('/admin/colissimo/test', async (req, res) => { + try { + const { contractNumber, password } = await getColissimoCredentials(); + // checkGenerateLabel = test sans générer de vrai colis + const testPayload = { + contractNumber, + password, + outputFormat: { x: 0, y: 0, outputPrintingType: 'PDF_10x15_300dpi' }, + letter: { + service: { + productCode: 'DOM', + depositDate: new Date().toISOString().split('T')[0] + }, + parcel: { weight: 0.5 }, + sender: { address: SENDER_ADDRESS }, + addressee: { + address: { + lastName: 'Test', + line2: '1 Rue de Rivoli', + countryCode: 'FR', + city: 'Paris', + zipCode: '75001' + } + } + } + }; + const response = await axios.post(`${COLISSIMO_BASE_URL}/checkGenerateLabel`, testPayload, { + headers: { 'Content-Type': 'application/json', Accept: '*/*' }, + responseType: 'arraybuffer', + timeout: 15000, + validateStatus: () => true + }); + const responseContentType = response.headers['content-type'] ?? ''; + const responseBuffer = Buffer.from(response.data); + // Si erreur HTTP, tenter de lire le body brut + if (response.status >= 400) { + let bodyText = responseBuffer.toString('utf-8').substring(0, 2000); + let parsed = null; + try { + parsed = JSON.parse(bodyText); + } + catch { /* ignore */ } + return res.status(response.status).json({ + connected: false, + httpStatus: response.status, + detail: parsed ?? bodyText + }); + } + const { json: labelJson } = parseColissimoMtomResponse(responseBuffer, responseContentType); + const messages = labelJson?.messages ?? []; + const hasError = messages.some((m) => m.type === 'ERROR'); + if (hasError) { + return res.status(400).json({ connected: false, messages }); + } + return res.json({ connected: true, messages }); + } + catch (err) { + const error = err; + const message = error?.message ? String(error.message) : 'unknown_error'; + console.error('GET /admin/colissimo/test failed', { message }); + if (message.includes('colissimo_credentials_missing')) { + return res.status(500).json({ connected: false, message }); + } + return res.status(502).json({ connected: false, message: 'colissimo_error', detail: message }); + } +}); +// --- SSE (Server-Sent Events) --- +const sseClients = new Set(); +app.get('/admin/events', (req, res) => { + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.setHeader('X-Accel-Buffering', 'no'); + res.flushHeaders(); + res.write('data: {"type":"connected"}\n\n'); + sseClients.add(res); + req.on('close', () => { + sseClients.delete(res); + }); +}); +const broadcastSSE = (eventType, data) => { + const payload = JSON.stringify({ type: eventType, ...data }); + for (const client of sseClients) { + try { + client.write(`data: ${payload}\n\n`); + } + catch { + sseClients.delete(client); + } + } +}; +// --- Amazon Polling --- +const POLL_INTERVAL_MS = Number(process.env.AMAZON_POLL_INTERVAL_MS) || 5 * 60 * 1000; +let lastPollAt = null; +let pollRunning = false; +const pollAmazonOrders = async () => { + if (pollRunning) { + console.log('[amazon-poll] Skipping, previous poll still running'); + return; + } + pollRunning = true; + const startedAt = new Date(); + let ordersFound = 0; + let ordersImported = 0; + let pollError = null; + try { + const db = getDb(); + let sp; + try { + sp = await createAmazonClient(); + } + catch { + // Pas de credentials configurées — skip silencieux + pollRunning = false; + return; + } + const createdAfter = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(); + const response = await sp.callAPI({ + operation: 'getOrders', + endpoint: 'orders', + query: { + MarketplaceIds: [AMAZON_MARKETPLACE_FR], + CreatedAfter: createdAfter, + OrderStatuses: ['Unshipped', 'PartiallyShipped'] + } + }); + const amazonOrders = response?.Orders ?? response?.payload?.Orders ?? []; + ordersFound = amazonOrders.length; + for (const amazonOrder of amazonOrders) { + const amazonOrderId = amazonOrder.AmazonOrderId; + if (!amazonOrderId) + continue; + const [existing] = await db + .select({ id: orders.id }) + .from(orders) + .where(eq(orders.orderRef, amazonOrderId)); + if (existing) + continue; + try { + const itemsResponse = await sp.callAPI({ + operation: 'getOrderItems', + endpoint: 'orders', + path: { orderId: amazonOrderId } + }); + const amazonItems = itemsResponse?.OrderItems ?? itemsResponse?.payload?.OrderItems ?? []; + if (amazonItems.length === 0) + continue; + const [newOrder] = await db + .insert(orders) + .values({ orderRef: amazonOrderId, status: 'new' }) + .returning(); + await db.insert(orderItems).values(amazonItems.map((item) => ({ + orderId: newOrder.id, + amazonSku: item.SellerSKU ?? 'UNKNOWN', + amazonOrderItemId: item.OrderItemId ?? null, + quantity: item.QuantityOrdered ?? 1, + title: item.Title ?? null + }))); + ordersImported++; + console.log(`[amazon-poll] Imported order ${amazonOrderId} -> ${newOrder.id}`); + broadcastSSE('amazon-order-imported', { + orderId: newOrder.id, + orderRef: amazonOrderId, + itemCount: amazonItems.length + }); + // Récupérer l'adresse de livraison + try { + await fetchAndStoreShippingAddress(sp, newOrder.id, amazonOrderId); + } + catch (addrErr) { + console.error(`[amazon-poll] Address fetch failed for ${amazonOrderId}:`, addrErr); + } + } + catch (err) { + const msg = err instanceof Error ? err.message : String(err); + // Doublon concurrent (import manuel + poll simultané) + if (err?.code === '23505') + continue; + console.error(`[amazon-poll] Failed to import ${amazonOrderId}:`, msg); + } + } + lastPollAt = new Date(); + if (ordersImported > 0) { + broadcastSSE('amazon-poll-complete', { ordersFound, ordersImported }); + } + } + catch (err) { + const msg = err instanceof Error ? err.message : String(err); + pollError = msg; + console.error('[amazon-poll] Poll failed:', msg); + } + finally { + try { + const db = getDb(); + await db.insert(amazonPollLog).values({ + startedAt, + finishedAt: new Date(), + ordersFound, + ordersImported, + error: pollError + }); + } + catch (logErr) { + console.error('[amazon-poll] Failed to write poll log:', logErr); + } + pollRunning = false; + } +}; +// Statut du polling Amazon +app.get('/admin/amazon/poll-status', async (_req, res) => { + try { + const db = getDb(); + const [lastLog] = await db + .select() + .from(amazonPollLog) + .orderBy(desc(amazonPollLog.startedAt)) + .limit(1); + return res.json({ + enabled: true, + intervalMs: POLL_INTERVAL_MS, + pollRunning, + lastPollAt, + lastLog: lastLog ?? null + }); + } + catch (err) { + console.error('GET /admin/amazon/poll-status failed', err); + return res.status(500).json({ message: 'internal_error' }); + } +}); +// Forcer un poll immédiat +app.post('/admin/amazon/poll-now', async (_req, res) => { + if (pollRunning) { + return res.status(409).json({ message: 'poll_already_running' }); + } + void pollAmazonOrders(); + return res.json({ message: 'poll_triggered' }); +}); +// --- Start --- +const port = Number(process.env.PORT ?? 4000); +const server = app.listen(port, () => { + console.log(`API listening on port ${port}`); + // Démarrer le polling Amazon après 30s + setTimeout(() => { + console.log(`[amazon-poll] Starting polling, interval: ${POLL_INTERVAL_MS}ms`); + void pollAmazonOrders(); + setInterval(() => void pollAmazonOrders(), POLL_INTERVAL_MS); + }, 30_000); +}); +// Graceful shutdown +const shutdown = async (signal) => { + console.log(`\n[shutdown] ${signal} received, closing...`); + server.close(); + const pool = getPool(); + if (pool) { + await pool.end(); + } + process.exit(0); +}; +process.on('SIGTERM', () => void shutdown('SIGTERM')); +process.on('SIGINT', () => void shutdown('SIGINT')); diff --git a/apps/api/dist/seed-admin.js b/apps/api/dist/seed-admin.js new file mode 100644 index 0000000..22a5d91 --- /dev/null +++ b/apps/api/dist/seed-admin.js @@ -0,0 +1,28 @@ +import bcrypt from 'bcryptjs'; +import { eq } from 'drizzle-orm'; +import { getDb } from './db/index.js'; +import { users } from './db/schema/index.js'; +const [, , username, password, displayName] = process.argv; +if (!username || !password || !displayName) { + console.error('Usage: pnpm -F @localiztoi/api seed:admin ""'); + process.exit(1); +} +const run = async () => { + const db = getDb(); + const [existing] = await db.select({ id: users.id }).from(users).where(eq(users.username, username)); + if (existing) { + console.log(`L'utilisateur "${username}" existe déjà.`); + process.exit(0); + } + const passwordHash = await bcrypt.hash(password, 12); + const [created] = await db + .insert(users) + .values({ username, passwordHash, displayName }) + .returning(); + console.log(`Admin créé : ${created.username} (${created.displayName}) — id: ${created.id}`); + process.exit(0); +}; +run().catch((err) => { + console.error('Erreur:', err); + process.exit(1); +}); diff --git a/apps/api/drizzle.config.ts b/apps/api/drizzle.config.ts new file mode 100644 index 0000000..a0ccacb --- /dev/null +++ b/apps/api/drizzle.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'drizzle-kit' + +export default defineConfig({ + schema: './src/db/schema/*', + out: './drizzle', + dialect: 'postgresql', + dbCredentials: { + url: process.env.DATABASE_URL ?? '' + } +}) diff --git a/apps/api/drizzle/0000_parallel_darkstar.sql b/apps/api/drizzle/0000_parallel_darkstar.sql new file mode 100644 index 0000000..1a20dde --- /dev/null +++ b/apps/api/drizzle/0000_parallel_darkstar.sql @@ -0,0 +1,11 @@ +CREATE TABLE "sku_mappings" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "amazon_sku" text NOT NULL, + "enterprise_sku" text NOT NULL, + "expected_fota_model" text NOT NULL, + "axonaut_product_internal_id" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX "sku_mappings_amazon_sku_unique" ON "sku_mappings" USING btree ("amazon_sku"); \ No newline at end of file diff --git a/apps/api/drizzle/0001_huge_senator_kelly.sql b/apps/api/drizzle/0001_huge_senator_kelly.sql new file mode 100644 index 0000000..4f1c377 --- /dev/null +++ b/apps/api/drizzle/0001_huge_senator_kelly.sql @@ -0,0 +1,18 @@ +CREATE TABLE "orders" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "order_ref" text NOT NULL, + "status" text DEFAULT 'new' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "order_items" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "order_id" uuid NOT NULL, + "amazon_sku" text NOT NULL, + "quantity" integer NOT NULL, + "title" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "order_items" ADD CONSTRAINT "order_items_order_id_orders_id_fk" FOREIGN KEY ("order_id") REFERENCES "public"."orders"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/apps/api/drizzle/0002_reflective_deathbird.sql b/apps/api/drizzle/0002_reflective_deathbird.sql new file mode 100644 index 0000000..4be926e --- /dev/null +++ b/apps/api/drizzle/0002_reflective_deathbird.sql @@ -0,0 +1,9 @@ +CREATE TABLE "order_imeis" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "order_id" uuid NOT NULL, + "imei" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "order_imeis" ADD CONSTRAINT "order_imeis_order_id_orders_id_fk" FOREIGN KEY ("order_id") REFERENCES "public"."orders"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "order_imeis_order_id_imei_unique" ON "order_imeis" USING btree ("order_id","imei"); \ No newline at end of file diff --git a/apps/api/drizzle/0003_lonely_turbo.sql b/apps/api/drizzle/0003_lonely_turbo.sql new file mode 100644 index 0000000..1a26e08 --- /dev/null +++ b/apps/api/drizzle/0003_lonely_turbo.sql @@ -0,0 +1,10 @@ +CREATE TABLE "api_keys" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "provider" text NOT NULL, + "label" text NOT NULL, + "value" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX "api_keys_provider_label_unique" ON "api_keys" USING btree ("provider","label"); \ No newline at end of file diff --git a/apps/api/drizzle/0004_naive_rattler.sql b/apps/api/drizzle/0004_naive_rattler.sql new file mode 100644 index 0000000..2ab5706 --- /dev/null +++ b/apps/api/drizzle/0004_naive_rattler.sql @@ -0,0 +1,13 @@ +ALTER TABLE "order_imeis" ADD COLUMN "fota_model" text;--> statement-breakpoint +ALTER TABLE "order_imeis" ADD COLUMN "fota_serial" text;--> statement-breakpoint +ALTER TABLE "order_imeis" ADD COLUMN "fota_current_firmware" text;--> statement-breakpoint +ALTER TABLE "order_imeis" ADD COLUMN "fota_activity_status" integer;--> statement-breakpoint +ALTER TABLE "order_imeis" ADD COLUMN "fota_seen_at" text;--> statement-breakpoint +ALTER TABLE "order_imeis" ADD COLUMN "fota_company_id" integer;--> statement-breakpoint +ALTER TABLE "order_imeis" ADD COLUMN "fota_company_name" text;--> statement-breakpoint +ALTER TABLE "order_imeis" ADD COLUMN "fota_group_id" integer;--> statement-breakpoint +ALTER TABLE "order_imeis" ADD COLUMN "fota_group_name" text;--> statement-breakpoint +ALTER TABLE "order_imeis" ADD COLUMN "fota_lookup_error" text;--> statement-breakpoint +ALTER TABLE "order_imeis" ADD COLUMN "fota_last_lookup_at" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "order_imeis" ADD COLUMN "fota_moved_at" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "order_imeis" ADD COLUMN "fota_move_error" text; \ No newline at end of file diff --git a/apps/api/drizzle/0005_nostalgic_shape.sql b/apps/api/drizzle/0005_nostalgic_shape.sql new file mode 100644 index 0000000..d8ad23d --- /dev/null +++ b/apps/api/drizzle/0005_nostalgic_shape.sql @@ -0,0 +1,2 @@ +ALTER TABLE "orders" ADD COLUMN "axonaut_destocked_at" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "orders" ADD COLUMN "axonaut_destock_error" text; \ No newline at end of file diff --git a/apps/api/drizzle/0006_pink_gorgon.sql b/apps/api/drizzle/0006_pink_gorgon.sql new file mode 100644 index 0000000..541d02e --- /dev/null +++ b/apps/api/drizzle/0006_pink_gorgon.sql @@ -0,0 +1,4 @@ +ALTER TABLE "orders" ADD COLUMN "colissimo_tracking_number" text;--> statement-breakpoint +ALTER TABLE "orders" ADD COLUMN "colissimo_label_url" text;--> statement-breakpoint +ALTER TABLE "orders" ADD COLUMN "colissimo_shipped_at" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "orders" ADD COLUMN "colissimo_error" text; \ No newline at end of file diff --git a/apps/api/drizzle/0007_lucky_karma.sql b/apps/api/drizzle/0007_lucky_karma.sql new file mode 100644 index 0000000..3de2beb --- /dev/null +++ b/apps/api/drizzle/0007_lucky_karma.sql @@ -0,0 +1,3 @@ +ALTER TABLE "orders" ADD COLUMN "amazon_tracking_confirmed_at" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "orders" ADD COLUMN "amazon_tracking_error" text;--> statement-breakpoint +ALTER TABLE "order_items" ADD COLUMN "amazon_order_item_id" text; \ No newline at end of file diff --git a/apps/api/drizzle/0008_striped_morg.sql b/apps/api/drizzle/0008_striped_morg.sql new file mode 100644 index 0000000..009a0f4 --- /dev/null +++ b/apps/api/drizzle/0008_striped_morg.sql @@ -0,0 +1,9 @@ +CREATE TABLE "amazon_poll_log" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "started_at" timestamp with time zone DEFAULT now() NOT NULL, + "finished_at" timestamp with time zone, + "orders_found" integer DEFAULT 0 NOT NULL, + "orders_imported" integer DEFAULT 0 NOT NULL, + "error" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); diff --git a/apps/api/drizzle/0009_sweet_wrecker.sql b/apps/api/drizzle/0009_sweet_wrecker.sql new file mode 100644 index 0000000..4277280 --- /dev/null +++ b/apps/api/drizzle/0009_sweet_wrecker.sql @@ -0,0 +1,9 @@ +CREATE TABLE "users" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "username" text NOT NULL, + "password_hash" text NOT NULL, + "display_name" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "users_username_unique" UNIQUE("username") +); diff --git a/apps/api/drizzle/0010_short_king_bedlam.sql b/apps/api/drizzle/0010_short_king_bedlam.sql new file mode 100644 index 0000000..73496bf --- /dev/null +++ b/apps/api/drizzle/0010_short_king_bedlam.sql @@ -0,0 +1,14 @@ +ALTER TABLE "orders" ADD COLUMN "shipping_name" text;--> statement-breakpoint +ALTER TABLE "orders" ADD COLUMN "shipping_first_name" text;--> statement-breakpoint +ALTER TABLE "orders" ADD COLUMN "shipping_last_name" text;--> statement-breakpoint +ALTER TABLE "orders" ADD COLUMN "shipping_line1" text;--> statement-breakpoint +ALTER TABLE "orders" ADD COLUMN "shipping_line2" text;--> statement-breakpoint +ALTER TABLE "orders" ADD COLUMN "shipping_line3" text;--> statement-breakpoint +ALTER TABLE "orders" ADD COLUMN "shipping_city" text;--> statement-breakpoint +ALTER TABLE "orders" ADD COLUMN "shipping_zip_code" text;--> statement-breakpoint +ALTER TABLE "orders" ADD COLUMN "shipping_country_code" text;--> statement-breakpoint +ALTER TABLE "orders" ADD COLUMN "shipping_state" text;--> statement-breakpoint +ALTER TABLE "orders" ADD COLUMN "shipping_phone" text;--> statement-breakpoint +ALTER TABLE "orders" ADD COLUMN "shipping_address_type" text;--> statement-breakpoint +ALTER TABLE "orders" ADD COLUMN "shipping_fetched_at" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "orders" ADD COLUMN "shipping_fetch_error" text; \ No newline at end of file diff --git a/apps/api/drizzle/meta/0000_snapshot.json b/apps/api/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..aed2797 --- /dev/null +++ b/apps/api/drizzle/meta/0000_snapshot.json @@ -0,0 +1,93 @@ +{ + "id": "1c5aa86b-4ec4-408e-92a0-4c32d375a8af", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.sku_mappings": { + "name": "sku_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "amazon_sku": { + "name": "amazon_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enterprise_sku": { + "name": "enterprise_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expected_fota_model": { + "name": "expected_fota_model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "axonaut_product_internal_id": { + "name": "axonaut_product_internal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sku_mappings_amazon_sku_unique": { + "name": "sku_mappings_amazon_sku_unique", + "columns": [ + { + "expression": "amazon_sku", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/drizzle/meta/0001_snapshot.json b/apps/api/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..115d36b --- /dev/null +++ b/apps/api/drizzle/meta/0001_snapshot.json @@ -0,0 +1,205 @@ +{ + "id": "0813536e-6aa4-4866-91ff-8439999a78dc", + "prevId": "1c5aa86b-4ec4-408e-92a0-4c32d375a8af", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.sku_mappings": { + "name": "sku_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "amazon_sku": { + "name": "amazon_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enterprise_sku": { + "name": "enterprise_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expected_fota_model": { + "name": "expected_fota_model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "axonaut_product_internal_id": { + "name": "axonaut_product_internal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sku_mappings_amazon_sku_unique": { + "name": "sku_mappings_amazon_sku_unique", + "columns": [ + { + "expression": "amazon_sku", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.orders": { + "name": "orders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_ref": { + "name": "order_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'new'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.order_items": { + "name": "order_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_id": { + "name": "order_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amazon_sku": { + "name": "amazon_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "order_items_order_id_orders_id_fk": { + "name": "order_items_order_id_orders_id_fk", + "tableFrom": "order_items", + "tableTo": "orders", + "columnsFrom": [ + "order_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/drizzle/meta/0002_snapshot.json b/apps/api/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..696ccf7 --- /dev/null +++ b/apps/api/drizzle/meta/0002_snapshot.json @@ -0,0 +1,280 @@ +{ + "id": "23f10660-2add-4f0b-81d9-421b4088d310", + "prevId": "0813536e-6aa4-4866-91ff-8439999a78dc", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.sku_mappings": { + "name": "sku_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "amazon_sku": { + "name": "amazon_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enterprise_sku": { + "name": "enterprise_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expected_fota_model": { + "name": "expected_fota_model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "axonaut_product_internal_id": { + "name": "axonaut_product_internal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sku_mappings_amazon_sku_unique": { + "name": "sku_mappings_amazon_sku_unique", + "columns": [ + { + "expression": "amazon_sku", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.orders": { + "name": "orders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_ref": { + "name": "order_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'new'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.order_items": { + "name": "order_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_id": { + "name": "order_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amazon_sku": { + "name": "amazon_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "order_items_order_id_orders_id_fk": { + "name": "order_items_order_id_orders_id_fk", + "tableFrom": "order_items", + "tableTo": "orders", + "columnsFrom": [ + "order_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.order_imeis": { + "name": "order_imeis", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_id": { + "name": "order_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "imei": { + "name": "imei", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "order_imeis_order_id_imei_unique": { + "name": "order_imeis_order_id_imei_unique", + "columns": [ + { + "expression": "order_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "imei", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "order_imeis_order_id_orders_id_fk": { + "name": "order_imeis_order_id_orders_id_fk", + "tableFrom": "order_imeis", + "tableTo": "orders", + "columnsFrom": [ + "order_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/drizzle/meta/0003_snapshot.json b/apps/api/drizzle/meta/0003_snapshot.json new file mode 100644 index 0000000..8ad003b --- /dev/null +++ b/apps/api/drizzle/meta/0003_snapshot.json @@ -0,0 +1,354 @@ +{ + "id": "a270275c-2536-41aa-8839-5de91e42e507", + "prevId": "23f10660-2add-4f0b-81d9-421b4088d310", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "api_keys_provider_label_unique": { + "name": "api_keys_provider_label_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "label", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sku_mappings": { + "name": "sku_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "amazon_sku": { + "name": "amazon_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enterprise_sku": { + "name": "enterprise_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expected_fota_model": { + "name": "expected_fota_model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "axonaut_product_internal_id": { + "name": "axonaut_product_internal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sku_mappings_amazon_sku_unique": { + "name": "sku_mappings_amazon_sku_unique", + "columns": [ + { + "expression": "amazon_sku", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.orders": { + "name": "orders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_ref": { + "name": "order_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'new'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.order_items": { + "name": "order_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_id": { + "name": "order_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amazon_sku": { + "name": "amazon_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "order_items_order_id_orders_id_fk": { + "name": "order_items_order_id_orders_id_fk", + "tableFrom": "order_items", + "tableTo": "orders", + "columnsFrom": [ + "order_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.order_imeis": { + "name": "order_imeis", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_id": { + "name": "order_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "imei": { + "name": "imei", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "order_imeis_order_id_imei_unique": { + "name": "order_imeis_order_id_imei_unique", + "columns": [ + { + "expression": "order_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "imei", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "order_imeis_order_id_orders_id_fk": { + "name": "order_imeis_order_id_orders_id_fk", + "tableFrom": "order_imeis", + "tableTo": "orders", + "columnsFrom": [ + "order_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/drizzle/meta/0004_snapshot.json b/apps/api/drizzle/meta/0004_snapshot.json new file mode 100644 index 0000000..8cc50fb --- /dev/null +++ b/apps/api/drizzle/meta/0004_snapshot.json @@ -0,0 +1,432 @@ +{ + "id": "3268bf9a-9588-4a2a-a0f6-7693e6bdd52c", + "prevId": "a270275c-2536-41aa-8839-5de91e42e507", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "api_keys_provider_label_unique": { + "name": "api_keys_provider_label_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "label", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sku_mappings": { + "name": "sku_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "amazon_sku": { + "name": "amazon_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enterprise_sku": { + "name": "enterprise_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expected_fota_model": { + "name": "expected_fota_model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "axonaut_product_internal_id": { + "name": "axonaut_product_internal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sku_mappings_amazon_sku_unique": { + "name": "sku_mappings_amazon_sku_unique", + "columns": [ + { + "expression": "amazon_sku", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.orders": { + "name": "orders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_ref": { + "name": "order_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'new'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.order_items": { + "name": "order_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_id": { + "name": "order_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amazon_sku": { + "name": "amazon_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "order_items_order_id_orders_id_fk": { + "name": "order_items_order_id_orders_id_fk", + "tableFrom": "order_items", + "tableTo": "orders", + "columnsFrom": [ + "order_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.order_imeis": { + "name": "order_imeis", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_id": { + "name": "order_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "imei": { + "name": "imei", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fota_model": { + "name": "fota_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_serial": { + "name": "fota_serial", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_current_firmware": { + "name": "fota_current_firmware", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_activity_status": { + "name": "fota_activity_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fota_seen_at": { + "name": "fota_seen_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_company_id": { + "name": "fota_company_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fota_company_name": { + "name": "fota_company_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_group_id": { + "name": "fota_group_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fota_group_name": { + "name": "fota_group_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_lookup_error": { + "name": "fota_lookup_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_last_lookup_at": { + "name": "fota_last_lookup_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "fota_moved_at": { + "name": "fota_moved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "fota_move_error": { + "name": "fota_move_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "order_imeis_order_id_imei_unique": { + "name": "order_imeis_order_id_imei_unique", + "columns": [ + { + "expression": "order_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "imei", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "order_imeis_order_id_orders_id_fk": { + "name": "order_imeis_order_id_orders_id_fk", + "tableFrom": "order_imeis", + "tableTo": "orders", + "columnsFrom": [ + "order_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/drizzle/meta/0005_snapshot.json b/apps/api/drizzle/meta/0005_snapshot.json new file mode 100644 index 0000000..e151120 --- /dev/null +++ b/apps/api/drizzle/meta/0005_snapshot.json @@ -0,0 +1,444 @@ +{ + "id": "64a0f718-3f97-4f0f-8c4b-29a43ba32f13", + "prevId": "3268bf9a-9588-4a2a-a0f6-7693e6bdd52c", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "api_keys_provider_label_unique": { + "name": "api_keys_provider_label_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "label", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sku_mappings": { + "name": "sku_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "amazon_sku": { + "name": "amazon_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enterprise_sku": { + "name": "enterprise_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expected_fota_model": { + "name": "expected_fota_model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "axonaut_product_internal_id": { + "name": "axonaut_product_internal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sku_mappings_amazon_sku_unique": { + "name": "sku_mappings_amazon_sku_unique", + "columns": [ + { + "expression": "amazon_sku", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.orders": { + "name": "orders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_ref": { + "name": "order_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'new'" + }, + "axonaut_destocked_at": { + "name": "axonaut_destocked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "axonaut_destock_error": { + "name": "axonaut_destock_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.order_items": { + "name": "order_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_id": { + "name": "order_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amazon_sku": { + "name": "amazon_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "order_items_order_id_orders_id_fk": { + "name": "order_items_order_id_orders_id_fk", + "tableFrom": "order_items", + "tableTo": "orders", + "columnsFrom": [ + "order_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.order_imeis": { + "name": "order_imeis", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_id": { + "name": "order_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "imei": { + "name": "imei", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fota_model": { + "name": "fota_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_serial": { + "name": "fota_serial", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_current_firmware": { + "name": "fota_current_firmware", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_activity_status": { + "name": "fota_activity_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fota_seen_at": { + "name": "fota_seen_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_company_id": { + "name": "fota_company_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fota_company_name": { + "name": "fota_company_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_group_id": { + "name": "fota_group_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fota_group_name": { + "name": "fota_group_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_lookup_error": { + "name": "fota_lookup_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_last_lookup_at": { + "name": "fota_last_lookup_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "fota_moved_at": { + "name": "fota_moved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "fota_move_error": { + "name": "fota_move_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "order_imeis_order_id_imei_unique": { + "name": "order_imeis_order_id_imei_unique", + "columns": [ + { + "expression": "order_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "imei", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "order_imeis_order_id_orders_id_fk": { + "name": "order_imeis_order_id_orders_id_fk", + "tableFrom": "order_imeis", + "tableTo": "orders", + "columnsFrom": [ + "order_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/drizzle/meta/0006_snapshot.json b/apps/api/drizzle/meta/0006_snapshot.json new file mode 100644 index 0000000..a6709eb --- /dev/null +++ b/apps/api/drizzle/meta/0006_snapshot.json @@ -0,0 +1,468 @@ +{ + "id": "9e83fede-35c3-417c-991d-7276404ecc1e", + "prevId": "64a0f718-3f97-4f0f-8c4b-29a43ba32f13", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "api_keys_provider_label_unique": { + "name": "api_keys_provider_label_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "label", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sku_mappings": { + "name": "sku_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "amazon_sku": { + "name": "amazon_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enterprise_sku": { + "name": "enterprise_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expected_fota_model": { + "name": "expected_fota_model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "axonaut_product_internal_id": { + "name": "axonaut_product_internal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sku_mappings_amazon_sku_unique": { + "name": "sku_mappings_amazon_sku_unique", + "columns": [ + { + "expression": "amazon_sku", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.orders": { + "name": "orders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_ref": { + "name": "order_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'new'" + }, + "axonaut_destocked_at": { + "name": "axonaut_destocked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "axonaut_destock_error": { + "name": "axonaut_destock_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "colissimo_tracking_number": { + "name": "colissimo_tracking_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "colissimo_label_url": { + "name": "colissimo_label_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "colissimo_shipped_at": { + "name": "colissimo_shipped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "colissimo_error": { + "name": "colissimo_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.order_items": { + "name": "order_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_id": { + "name": "order_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amazon_sku": { + "name": "amazon_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "order_items_order_id_orders_id_fk": { + "name": "order_items_order_id_orders_id_fk", + "tableFrom": "order_items", + "tableTo": "orders", + "columnsFrom": [ + "order_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.order_imeis": { + "name": "order_imeis", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_id": { + "name": "order_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "imei": { + "name": "imei", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fota_model": { + "name": "fota_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_serial": { + "name": "fota_serial", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_current_firmware": { + "name": "fota_current_firmware", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_activity_status": { + "name": "fota_activity_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fota_seen_at": { + "name": "fota_seen_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_company_id": { + "name": "fota_company_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fota_company_name": { + "name": "fota_company_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_group_id": { + "name": "fota_group_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fota_group_name": { + "name": "fota_group_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_lookup_error": { + "name": "fota_lookup_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_last_lookup_at": { + "name": "fota_last_lookup_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "fota_moved_at": { + "name": "fota_moved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "fota_move_error": { + "name": "fota_move_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "order_imeis_order_id_imei_unique": { + "name": "order_imeis_order_id_imei_unique", + "columns": [ + { + "expression": "order_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "imei", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "order_imeis_order_id_orders_id_fk": { + "name": "order_imeis_order_id_orders_id_fk", + "tableFrom": "order_imeis", + "tableTo": "orders", + "columnsFrom": [ + "order_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/drizzle/meta/0007_snapshot.json b/apps/api/drizzle/meta/0007_snapshot.json new file mode 100644 index 0000000..54d7ce6 --- /dev/null +++ b/apps/api/drizzle/meta/0007_snapshot.json @@ -0,0 +1,486 @@ +{ + "id": "30dc85d4-0af0-491a-a23d-8f8f1e8ef38b", + "prevId": "9e83fede-35c3-417c-991d-7276404ecc1e", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "api_keys_provider_label_unique": { + "name": "api_keys_provider_label_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "label", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sku_mappings": { + "name": "sku_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "amazon_sku": { + "name": "amazon_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enterprise_sku": { + "name": "enterprise_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expected_fota_model": { + "name": "expected_fota_model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "axonaut_product_internal_id": { + "name": "axonaut_product_internal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sku_mappings_amazon_sku_unique": { + "name": "sku_mappings_amazon_sku_unique", + "columns": [ + { + "expression": "amazon_sku", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.orders": { + "name": "orders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_ref": { + "name": "order_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'new'" + }, + "axonaut_destocked_at": { + "name": "axonaut_destocked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "axonaut_destock_error": { + "name": "axonaut_destock_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "colissimo_tracking_number": { + "name": "colissimo_tracking_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "colissimo_label_url": { + "name": "colissimo_label_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "colissimo_shipped_at": { + "name": "colissimo_shipped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "colissimo_error": { + "name": "colissimo_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amazon_tracking_confirmed_at": { + "name": "amazon_tracking_confirmed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "amazon_tracking_error": { + "name": "amazon_tracking_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.order_items": { + "name": "order_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_id": { + "name": "order_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amazon_sku": { + "name": "amazon_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amazon_order_item_id": { + "name": "amazon_order_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "order_items_order_id_orders_id_fk": { + "name": "order_items_order_id_orders_id_fk", + "tableFrom": "order_items", + "tableTo": "orders", + "columnsFrom": [ + "order_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.order_imeis": { + "name": "order_imeis", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_id": { + "name": "order_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "imei": { + "name": "imei", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fota_model": { + "name": "fota_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_serial": { + "name": "fota_serial", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_current_firmware": { + "name": "fota_current_firmware", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_activity_status": { + "name": "fota_activity_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fota_seen_at": { + "name": "fota_seen_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_company_id": { + "name": "fota_company_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fota_company_name": { + "name": "fota_company_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_group_id": { + "name": "fota_group_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fota_group_name": { + "name": "fota_group_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_lookup_error": { + "name": "fota_lookup_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_last_lookup_at": { + "name": "fota_last_lookup_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "fota_moved_at": { + "name": "fota_moved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "fota_move_error": { + "name": "fota_move_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "order_imeis_order_id_imei_unique": { + "name": "order_imeis_order_id_imei_unique", + "columns": [ + { + "expression": "order_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "imei", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "order_imeis_order_id_orders_id_fk": { + "name": "order_imeis_order_id_orders_id_fk", + "tableFrom": "order_imeis", + "tableTo": "orders", + "columnsFrom": [ + "order_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/drizzle/meta/0008_snapshot.json b/apps/api/drizzle/meta/0008_snapshot.json new file mode 100644 index 0000000..45396e2 --- /dev/null +++ b/apps/api/drizzle/meta/0008_snapshot.json @@ -0,0 +1,546 @@ +{ + "id": "4fd753b1-8217-4793-a994-33b8c7dacd68", + "prevId": "30dc85d4-0af0-491a-a23d-8f8f1e8ef38b", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.amazon_poll_log": { + "name": "amazon_poll_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "orders_found": { + "name": "orders_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "orders_imported": { + "name": "orders_imported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "api_keys_provider_label_unique": { + "name": "api_keys_provider_label_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "label", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sku_mappings": { + "name": "sku_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "amazon_sku": { + "name": "amazon_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enterprise_sku": { + "name": "enterprise_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expected_fota_model": { + "name": "expected_fota_model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "axonaut_product_internal_id": { + "name": "axonaut_product_internal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sku_mappings_amazon_sku_unique": { + "name": "sku_mappings_amazon_sku_unique", + "columns": [ + { + "expression": "amazon_sku", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.orders": { + "name": "orders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_ref": { + "name": "order_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'new'" + }, + "axonaut_destocked_at": { + "name": "axonaut_destocked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "axonaut_destock_error": { + "name": "axonaut_destock_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "colissimo_tracking_number": { + "name": "colissimo_tracking_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "colissimo_label_url": { + "name": "colissimo_label_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "colissimo_shipped_at": { + "name": "colissimo_shipped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "colissimo_error": { + "name": "colissimo_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amazon_tracking_confirmed_at": { + "name": "amazon_tracking_confirmed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "amazon_tracking_error": { + "name": "amazon_tracking_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.order_items": { + "name": "order_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_id": { + "name": "order_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amazon_sku": { + "name": "amazon_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amazon_order_item_id": { + "name": "amazon_order_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "order_items_order_id_orders_id_fk": { + "name": "order_items_order_id_orders_id_fk", + "tableFrom": "order_items", + "tableTo": "orders", + "columnsFrom": [ + "order_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.order_imeis": { + "name": "order_imeis", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_id": { + "name": "order_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "imei": { + "name": "imei", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fota_model": { + "name": "fota_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_serial": { + "name": "fota_serial", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_current_firmware": { + "name": "fota_current_firmware", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_activity_status": { + "name": "fota_activity_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fota_seen_at": { + "name": "fota_seen_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_company_id": { + "name": "fota_company_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fota_company_name": { + "name": "fota_company_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_group_id": { + "name": "fota_group_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fota_group_name": { + "name": "fota_group_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_lookup_error": { + "name": "fota_lookup_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_last_lookup_at": { + "name": "fota_last_lookup_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "fota_moved_at": { + "name": "fota_moved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "fota_move_error": { + "name": "fota_move_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "order_imeis_order_id_imei_unique": { + "name": "order_imeis_order_id_imei_unique", + "columns": [ + { + "expression": "order_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "imei", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "order_imeis_order_id_orders_id_fk": { + "name": "order_imeis_order_id_orders_id_fk", + "tableFrom": "order_imeis", + "tableTo": "orders", + "columnsFrom": [ + "order_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/drizzle/meta/0009_snapshot.json b/apps/api/drizzle/meta/0009_snapshot.json new file mode 100644 index 0000000..13413f1 --- /dev/null +++ b/apps/api/drizzle/meta/0009_snapshot.json @@ -0,0 +1,606 @@ +{ + "id": "406c03e2-8698-493f-ba3e-ee8ca42062fe", + "prevId": "4fd753b1-8217-4793-a994-33b8c7dacd68", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.amazon_poll_log": { + "name": "amazon_poll_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "orders_found": { + "name": "orders_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "orders_imported": { + "name": "orders_imported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "api_keys_provider_label_unique": { + "name": "api_keys_provider_label_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "label", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sku_mappings": { + "name": "sku_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "amazon_sku": { + "name": "amazon_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enterprise_sku": { + "name": "enterprise_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expected_fota_model": { + "name": "expected_fota_model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "axonaut_product_internal_id": { + "name": "axonaut_product_internal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sku_mappings_amazon_sku_unique": { + "name": "sku_mappings_amazon_sku_unique", + "columns": [ + { + "expression": "amazon_sku", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.orders": { + "name": "orders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_ref": { + "name": "order_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'new'" + }, + "axonaut_destocked_at": { + "name": "axonaut_destocked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "axonaut_destock_error": { + "name": "axonaut_destock_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "colissimo_tracking_number": { + "name": "colissimo_tracking_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "colissimo_label_url": { + "name": "colissimo_label_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "colissimo_shipped_at": { + "name": "colissimo_shipped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "colissimo_error": { + "name": "colissimo_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amazon_tracking_confirmed_at": { + "name": "amazon_tracking_confirmed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "amazon_tracking_error": { + "name": "amazon_tracking_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.order_items": { + "name": "order_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_id": { + "name": "order_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amazon_sku": { + "name": "amazon_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amazon_order_item_id": { + "name": "amazon_order_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "order_items_order_id_orders_id_fk": { + "name": "order_items_order_id_orders_id_fk", + "tableFrom": "order_items", + "tableTo": "orders", + "columnsFrom": [ + "order_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.order_imeis": { + "name": "order_imeis", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_id": { + "name": "order_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "imei": { + "name": "imei", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fota_model": { + "name": "fota_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_serial": { + "name": "fota_serial", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_current_firmware": { + "name": "fota_current_firmware", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_activity_status": { + "name": "fota_activity_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fota_seen_at": { + "name": "fota_seen_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_company_id": { + "name": "fota_company_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fota_company_name": { + "name": "fota_company_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_group_id": { + "name": "fota_group_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fota_group_name": { + "name": "fota_group_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_lookup_error": { + "name": "fota_lookup_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_last_lookup_at": { + "name": "fota_last_lookup_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "fota_moved_at": { + "name": "fota_moved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "fota_move_error": { + "name": "fota_move_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "order_imeis_order_id_imei_unique": { + "name": "order_imeis_order_id_imei_unique", + "columns": [ + { + "expression": "order_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "imei", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "order_imeis_order_id_orders_id_fk": { + "name": "order_imeis_order_id_orders_id_fk", + "tableFrom": "order_imeis", + "tableTo": "orders", + "columnsFrom": [ + "order_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/drizzle/meta/0010_snapshot.json b/apps/api/drizzle/meta/0010_snapshot.json new file mode 100644 index 0000000..cb3c2b8 --- /dev/null +++ b/apps/api/drizzle/meta/0010_snapshot.json @@ -0,0 +1,690 @@ +{ + "id": "4dab07de-4804-4198-8d86-01b7c2f9dcaa", + "prevId": "406c03e2-8698-493f-ba3e-ee8ca42062fe", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.amazon_poll_log": { + "name": "amazon_poll_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "orders_found": { + "name": "orders_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "orders_imported": { + "name": "orders_imported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "api_keys_provider_label_unique": { + "name": "api_keys_provider_label_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "label", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sku_mappings": { + "name": "sku_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "amazon_sku": { + "name": "amazon_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enterprise_sku": { + "name": "enterprise_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expected_fota_model": { + "name": "expected_fota_model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "axonaut_product_internal_id": { + "name": "axonaut_product_internal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sku_mappings_amazon_sku_unique": { + "name": "sku_mappings_amazon_sku_unique", + "columns": [ + { + "expression": "amazon_sku", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.orders": { + "name": "orders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_ref": { + "name": "order_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'new'" + }, + "axonaut_destocked_at": { + "name": "axonaut_destocked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "axonaut_destock_error": { + "name": "axonaut_destock_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "colissimo_tracking_number": { + "name": "colissimo_tracking_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "colissimo_label_url": { + "name": "colissimo_label_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "colissimo_shipped_at": { + "name": "colissimo_shipped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "colissimo_error": { + "name": "colissimo_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amazon_tracking_confirmed_at": { + "name": "amazon_tracking_confirmed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "amazon_tracking_error": { + "name": "amazon_tracking_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shipping_name": { + "name": "shipping_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shipping_first_name": { + "name": "shipping_first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shipping_last_name": { + "name": "shipping_last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shipping_line1": { + "name": "shipping_line1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shipping_line2": { + "name": "shipping_line2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shipping_line3": { + "name": "shipping_line3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shipping_city": { + "name": "shipping_city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shipping_zip_code": { + "name": "shipping_zip_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shipping_country_code": { + "name": "shipping_country_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shipping_state": { + "name": "shipping_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shipping_phone": { + "name": "shipping_phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shipping_address_type": { + "name": "shipping_address_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shipping_fetched_at": { + "name": "shipping_fetched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "shipping_fetch_error": { + "name": "shipping_fetch_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.order_items": { + "name": "order_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_id": { + "name": "order_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amazon_sku": { + "name": "amazon_sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amazon_order_item_id": { + "name": "amazon_order_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "order_items_order_id_orders_id_fk": { + "name": "order_items_order_id_orders_id_fk", + "tableFrom": "order_items", + "tableTo": "orders", + "columnsFrom": [ + "order_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.order_imeis": { + "name": "order_imeis", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_id": { + "name": "order_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "imei": { + "name": "imei", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fota_model": { + "name": "fota_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_serial": { + "name": "fota_serial", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_current_firmware": { + "name": "fota_current_firmware", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_activity_status": { + "name": "fota_activity_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fota_seen_at": { + "name": "fota_seen_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_company_id": { + "name": "fota_company_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fota_company_name": { + "name": "fota_company_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_group_id": { + "name": "fota_group_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fota_group_name": { + "name": "fota_group_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_lookup_error": { + "name": "fota_lookup_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fota_last_lookup_at": { + "name": "fota_last_lookup_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "fota_moved_at": { + "name": "fota_moved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "fota_move_error": { + "name": "fota_move_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "order_imeis_order_id_imei_unique": { + "name": "order_imeis_order_id_imei_unique", + "columns": [ + { + "expression": "order_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "imei", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "order_imeis_order_id_orders_id_fk": { + "name": "order_imeis_order_id_orders_id_fk", + "tableFrom": "order_imeis", + "tableTo": "orders", + "columnsFrom": [ + "order_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/drizzle/meta/_journal.json b/apps/api/drizzle/meta/_journal.json new file mode 100644 index 0000000..62fa148 --- /dev/null +++ b/apps/api/drizzle/meta/_journal.json @@ -0,0 +1,83 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1771328005091, + "tag": "0000_parallel_darkstar", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1771329960889, + "tag": "0001_huge_senator_kelly", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1771330499929, + "tag": "0002_reflective_deathbird", + "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1771405468186, + "tag": "0003_lonely_turbo", + "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1771416713614, + "tag": "0004_naive_rattler", + "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1771432314879, + "tag": "0005_nostalgic_shape", + "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1771496458805, + "tag": "0006_pink_gorgon", + "breakpoints": true + }, + { + "idx": 7, + "version": "7", + "when": 1771500645465, + "tag": "0007_lucky_karma", + "breakpoints": true + }, + { + "idx": 8, + "version": "7", + "when": 1771525119636, + "tag": "0008_striped_morg", + "breakpoints": true + }, + { + "idx": 9, + "version": "7", + "when": 1771527796416, + "tag": "0009_sweet_wrecker", + "breakpoints": true + }, + { + "idx": 10, + "version": "7", + "when": 1771530682623, + "tag": "0010_short_king_bedlam", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..3274007 --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,41 @@ +{ + "name": "@localiztoi/api", + "private": true, + "type": "module", + "scripts": { + "dev": "dotenv -e .env -- tsx watch src/index.ts", + "build": "tsc -p tsconfig.json", + "start": "node dist/index.js", + "db:generate": "dotenv -e .env -- drizzle-kit generate", + "db:migrate": "dotenv -e .env -- drizzle-kit migrate", + "db:push": "dotenv -e .env -- drizzle-kit push", + "seed:admin": "dotenv -e .env -- tsx src/seed-admin.ts", + "lint": "echo \"lint not configured yet\"" + }, + "dependencies": { + "@localiztoi/shared": "workspace:*", + "amazon-sp-api": "^1.2.0", + "axios": "^1.13.5", + "bcryptjs": "^3.0.3", + "cookie-parser": "^1.4.7", + "cors": "^2.8.6", + "drizzle-orm": "^0.45.1", + "express": "^5.2.1", + "jsonwebtoken": "^9.0.3", + "pg": "^8.18.0", + "zod": "^4.3.6" + }, + "devDependencies": { + "@types/bcryptjs": "^3.0.0", + "@types/cookie-parser": "^1.4.10", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^20.19.33", + "@types/pg": "^8.16.0", + "dotenv-cli": "^11.0.0", + "drizzle-kit": "^0.31.9", + "tsx": "^4.21.0", + "typescript": "^5.9.3" + } +} diff --git a/apps/api/src/db/index.ts b/apps/api/src/db/index.ts new file mode 100644 index 0000000..1f032ca --- /dev/null +++ b/apps/api/src/db/index.ts @@ -0,0 +1,30 @@ +import { drizzle } from 'drizzle-orm/node-postgres' +import pg from 'pg' + +const { Pool } = pg + +let singletonPool: pg.Pool | null = null + +export const getPool = () => singletonPool + +export const getDb = () => { + const databaseUrl = process.env.DATABASE_URL + if (!databaseUrl) { + throw new Error('DATABASE_URL is required') + } + + if (!singletonPool) { + singletonPool = new Pool({ + connectionString: databaseUrl, + max: 10, + idleTimeoutMillis: 30_000, + connectionTimeoutMillis: 5_000 + }) + + singletonPool.on('error', (err) => { + console.error('[db] Pool background error', err.message) + }) + } + + return drizzle(singletonPool) +} diff --git a/apps/api/src/db/schema/amazonPollLog.ts b/apps/api/src/db/schema/amazonPollLog.ts new file mode 100644 index 0000000..a10a750 --- /dev/null +++ b/apps/api/src/db/schema/amazonPollLog.ts @@ -0,0 +1,11 @@ +import { integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' + +export const amazonPollLog = pgTable('amazon_poll_log', { + id: uuid('id').primaryKey().defaultRandom(), + startedAt: timestamp('started_at', { withTimezone: true }).notNull().defaultNow(), + finishedAt: timestamp('finished_at', { withTimezone: true }), + ordersFound: integer('orders_found').notNull().default(0), + ordersImported: integer('orders_imported').notNull().default(0), + error: text('error'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), +}) diff --git a/apps/api/src/db/schema/apiKeys.ts b/apps/api/src/db/schema/apiKeys.ts new file mode 100644 index 0000000..8433c6f --- /dev/null +++ b/apps/api/src/db/schema/apiKeys.ts @@ -0,0 +1,20 @@ +import { pgTable, text, timestamp, uniqueIndex, uuid } from 'drizzle-orm/pg-core' + +export const apiKeys = pgTable( + 'api_keys', + { + id: uuid('id').primaryKey().defaultRandom(), + provider: text('provider').notNull(), + label: text('label').notNull(), + value: text('value').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }) + .notNull() + .defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }) + .notNull() + .defaultNow() + }, + (t) => ({ + providerLabelUniqueIdx: uniqueIndex('api_keys_provider_label_unique').on(t.provider, t.label) + }) +) diff --git a/apps/api/src/db/schema/index.ts b/apps/api/src/db/schema/index.ts new file mode 100644 index 0000000..415906d --- /dev/null +++ b/apps/api/src/db/schema/index.ts @@ -0,0 +1,7 @@ +export * from './skuMappings.js' +export * from './orders.js' +export * from './orderItems.js' +export * from './orderImeis.js' +export * from './apiKeys.js' +export * from './amazonPollLog.js' +export * from './users.js' diff --git a/apps/api/src/db/schema/orderImeis.ts b/apps/api/src/db/schema/orderImeis.ts new file mode 100644 index 0000000..41cf788 --- /dev/null +++ b/apps/api/src/db/schema/orderImeis.ts @@ -0,0 +1,33 @@ +import { integer, pgTable, text, timestamp, uniqueIndex, uuid } from 'drizzle-orm/pg-core' + +import { orders } from './orders.js' + +export const orderImeis = pgTable( + 'order_imeis', + { + id: uuid('id').primaryKey().defaultRandom(), + orderId: uuid('order_id') + .notNull() + .references(() => orders.id, { onDelete: 'cascade' }), + imei: text('imei').notNull(), + fotaModel: text('fota_model'), + fotaSerial: text('fota_serial'), + fotaCurrentFirmware: text('fota_current_firmware'), + fotaActivityStatus: integer('fota_activity_status'), + fotaSeenAt: text('fota_seen_at'), + fotaCompanyId: integer('fota_company_id'), + fotaCompanyName: text('fota_company_name'), + fotaGroupId: integer('fota_group_id'), + fotaGroupName: text('fota_group_name'), + fotaLookupError: text('fota_lookup_error'), + fotaLastLookupAt: timestamp('fota_last_lookup_at', { withTimezone: true }), + fotaMovedAt: timestamp('fota_moved_at', { withTimezone: true }), + fotaMoveError: text('fota_move_error'), + createdAt: timestamp('created_at', { withTimezone: true }) + .notNull() + .defaultNow() + }, + (t) => ({ + orderImeiUniqueIdx: uniqueIndex('order_imeis_order_id_imei_unique').on(t.orderId, t.imei) + }) +) diff --git a/apps/api/src/db/schema/orderItems.ts b/apps/api/src/db/schema/orderItems.ts new file mode 100644 index 0000000..d562894 --- /dev/null +++ b/apps/api/src/db/schema/orderItems.ts @@ -0,0 +1,17 @@ +import { integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' + +import { orders } from './orders.js' + +export const orderItems = pgTable('order_items', { + id: uuid('id').primaryKey().defaultRandom(), + orderId: uuid('order_id') + .notNull() + .references(() => orders.id, { onDelete: 'cascade' }), + amazonSku: text('amazon_sku').notNull(), + amazonOrderItemId: text('amazon_order_item_id'), + quantity: integer('quantity').notNull(), + title: text('title'), + createdAt: timestamp('created_at', { withTimezone: true }) + .notNull() + .defaultNow() +}) diff --git a/apps/api/src/db/schema/orders.ts b/apps/api/src/db/schema/orders.ts new file mode 100644 index 0000000..539a218 --- /dev/null +++ b/apps/api/src/db/schema/orders.ts @@ -0,0 +1,35 @@ +import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' + +export const orders = pgTable('orders', { + id: uuid('id').primaryKey().defaultRandom(), + orderRef: text('order_ref').notNull(), + status: text('status').notNull().default('new'), + axonautDestockedAt: timestamp('axonaut_destocked_at', { withTimezone: true }), + axonautDestockError: text('axonaut_destock_error'), + colissimoTrackingNumber: text('colissimo_tracking_number'), + colissimoLabelUrl: text('colissimo_label_url'), + colissimoShippedAt: timestamp('colissimo_shipped_at', { withTimezone: true }), + colissimoError: text('colissimo_error'), + amazonTrackingConfirmedAt: timestamp('amazon_tracking_confirmed_at', { withTimezone: true }), + amazonTrackingError: text('amazon_tracking_error'), + shippingName: text('shipping_name'), + shippingFirstName: text('shipping_first_name'), + shippingLastName: text('shipping_last_name'), + shippingLine1: text('shipping_line1'), + shippingLine2: text('shipping_line2'), + shippingLine3: text('shipping_line3'), + shippingCity: text('shipping_city'), + shippingZipCode: text('shipping_zip_code'), + shippingCountryCode: text('shipping_country_code'), + shippingState: text('shipping_state'), + shippingPhone: text('shipping_phone'), + shippingAddressType: text('shipping_address_type'), + shippingFetchedAt: timestamp('shipping_fetched_at', { withTimezone: true }), + shippingFetchError: text('shipping_fetch_error'), + createdAt: timestamp('created_at', { withTimezone: true }) + .notNull() + .defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }) + .notNull() + .defaultNow() +}) diff --git a/apps/api/src/db/schema/skuMappings.ts b/apps/api/src/db/schema/skuMappings.ts new file mode 100644 index 0000000..344d381 --- /dev/null +++ b/apps/api/src/db/schema/skuMappings.ts @@ -0,0 +1,21 @@ +import { pgTable, text, timestamp, uniqueIndex, uuid } from 'drizzle-orm/pg-core' + +export const skuMappings = pgTable( + 'sku_mappings', + { + id: uuid('id').primaryKey().defaultRandom(), + amazonSku: text('amazon_sku').notNull(), + enterpriseSku: text('enterprise_sku').notNull(), + expectedFotaModel: text('expected_fota_model').notNull(), + axonautProductInternalId: text('axonaut_product_internal_id').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }) + .notNull() + .defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }) + .notNull() + .defaultNow() + }, + (t) => ({ + amazonSkuUniqueIdx: uniqueIndex('sku_mappings_amazon_sku_unique').on(t.amazonSku) + }) +) diff --git a/apps/api/src/db/schema/users.ts b/apps/api/src/db/schema/users.ts new file mode 100644 index 0000000..be311f1 --- /dev/null +++ b/apps/api/src/db/schema/users.ts @@ -0,0 +1,10 @@ +import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' + +export const users = pgTable('users', { + id: uuid('id').primaryKey().defaultRandom(), + username: text('username').notNull().unique(), + passwordHash: text('password_hash').notNull(), + displayName: text('display_name').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), +}) diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts new file mode 100644 index 0000000..ddbedad --- /dev/null +++ b/apps/api/src/index.ts @@ -0,0 +1,2736 @@ +import cors from 'cors' +import express from 'express' +import axios from 'axios' +import jwt from 'jsonwebtoken' +import bcrypt from 'bcryptjs' +import cookieParser from 'cookie-parser' +import { and, desc, eq, inArray, sql } from 'drizzle-orm' +import { orderCreateSchema, scanImeiSchema, skuMappingSchema, apiKeyCreateSchema, apiKeyUpdateSchema, loginSchema, registerSchema, changePasswordSchema } from '@localiztoi/shared' +import type { AuthUser } from '@localiztoi/shared' + +import { getDb, getPool } from './db/index.js' +import { orderImeis, orderItems, orders, skuMappings, apiKeys, amazonPollLog, users } from './db/schema/index.js' + +const app = express() + +// CORS avec credentials (cookies) +const CORS_ORIGIN = process.env.CORS_ORIGIN ?? 'http://localhost:3000' +const allowedOrigins = CORS_ORIGIN.split(',').map((o) => o.trim()) + +app.use(cors({ + origin: (origin, callback) => { + if (!origin || allowedOrigins.includes(origin)) { + callback(null, true) + } else { + callback(new Error('Not allowed by CORS')) + } + }, + credentials: true +})) +app.use(express.json()) +app.use(cookieParser()) + +// --- JWT helpers --- + +const JWT_SECRET = process.env.JWT_SECRET +const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN ?? '7d' +const COOKIE_SECURE = process.env.COOKIE_SECURE === 'true' + +const signToken = (user: AuthUser): string => { + if (!JWT_SECRET) throw new Error('JWT_SECRET is not configured') + return jwt.sign({ sub: user.id, username: user.username, displayName: user.displayName }, JWT_SECRET, { + expiresIn: JWT_EXPIRES_IN as jwt.SignOptions['expiresIn'] + }) +} + +const setAuthCookie = (res: express.Response, token: string) => { + res.cookie('auth_token', token, { + httpOnly: true, + secure: COOKIE_SECURE, + sameSite: 'lax', + maxAge: 7 * 24 * 60 * 60 * 1000, // 7 jours + path: '/' + }) +} + +const verifyToken = (token: string): AuthUser | null => { + if (!JWT_SECRET) return null + try { + const payload = jwt.verify(token, JWT_SECRET) as any + return { + id: payload.sub, + username: payload.username, + displayName: payload.displayName + } + } catch { + return null + } +} + +// Augmenter le type Request pour inclure user +declare global { + namespace Express { + interface Request { + user?: AuthUser + } + } +} + +const teltonikaBaseUrl = 'https://api.teltonika.lt' + +const getTeltonikaAuthHeaders = () => { + const token = process.env.TELTONIKA_FOTA_API_TOKEN + if (!token) { + throw new Error('TELTONIKA_FOTA_API_TOKEN is required') + } + + const userAgent = process.env.TELTONIKA_FOTA_USER_AGENT + if (!userAgent) { + throw new Error('TELTONIKA_FOTA_USER_AGENT is required') + } + + return { + Authorization: `Bearer ${token}`, + 'User-Agent': userAgent, + Accept: 'application/json', + 'Content-Type': 'application/json' + } +} + +const teltonikaClient = axios.create({ + baseURL: teltonikaBaseUrl, + timeout: 15000 +}) + +const toMaybeInt64 = (value: string) => { + const cleaned = value.trim() + if (!cleaned) return null + const n = Number(cleaned) + if (!Number.isFinite(n)) return null + if (!Number.isInteger(n)) return null + return n +} + +const isMaybeImei = (value: string) => { + const cleaned = value.trim() + if (!cleaned) return false + if (!/^[0-9]+$/.test(cleaned)) return false + return cleaned.length === 15 +} + +const normalizeFotaActivityStatus = (value: unknown) => { + if (value === null || typeof value === 'undefined') { + return null + } + + if (typeof value === 'number' && Number.isFinite(value)) { + return value + } + + if (typeof value === 'string') { + const cleaned = value.trim().toLowerCase() + if (cleaned === 'online') return 2 + if (cleaned === 'offline') return 1 + if (cleaned === 'inactive') return 0 + + const asNumber = Number(cleaned) + if (Number.isFinite(asNumber) && Number.isInteger(asNumber)) { + return asNumber + } + } + + return null +} + +const lookupTeltonikaDeviceByImei = async (imei: string) => { + const numericImei = toMaybeInt64(imei) + if (numericImei === null) { + throw new Error('invalid_imei') + } + + const res = await teltonikaClient.get(`/devices/${numericImei}`, { + headers: getTeltonikaAuthHeaders() + }) + + return res.data as any +} + +const extractFirstDeviceFromPagedResult = (payload: any) => { + if (!payload) return null + if (Array.isArray(payload)) return payload[0] ?? null + if (Array.isArray(payload.data)) return payload.data[0] ?? null + if (Array.isArray(payload.items)) return payload.items[0] ?? null + if (Array.isArray(payload.results)) return payload.results[0] ?? null + return null +} + +const resolveTeltonikaDeviceByIdentifier = async (identifier: string) => { + const cleaned = identifier.trim() + if (!cleaned) { + throw new Error('invalid_identifier') + } + + if (isMaybeImei(cleaned)) { + const numericImei = toMaybeInt64(cleaned) + if (numericImei === null) { + throw new Error('invalid_imei') + } + + const device = await lookupTeltonikaDeviceByImei(cleaned) + return { + imei: String(numericImei), + device + } + } + + const res = await teltonikaClient.get('/devices', { + headers: getTeltonikaAuthHeaders(), + params: { + query: cleaned, + query_field: 'serial', + per_page: 1, + page: 1 + } + }) + + const device = extractFirstDeviceFromPagedResult(res.data) + const deviceImei = device?.imei ?? device?.device_imei + const numeric = typeof deviceImei === 'number' ? deviceImei : toMaybeInt64(String(deviceImei ?? '')) + if (numeric === null) { + throw new Error('device_not_found') + } + + return { + imei: String(numeric), + device + } +} + +app.get('/health', (_req, res) => { + res.json({ ok: true }) +}) + +// --- Auth routes --- + +app.post('/auth/login', async (req, res) => { + const parsed = loginSchema.safeParse(req.body) + if (!parsed.success) { + return res.status(400).json({ message: 'invalid_body', issues: parsed.error.issues }) + } + + try { + const db = getDb() + const [user] = await db + .select() + .from(users) + .where(eq(users.username, parsed.data.username)) + + if (!user) { + return res.status(401).json({ message: 'invalid_credentials' }) + } + + const validPassword = await bcrypt.compare(parsed.data.password, user.passwordHash) + if (!validPassword) { + return res.status(401).json({ message: 'invalid_credentials' }) + } + + const authUser: AuthUser = { id: user.id, username: user.username, displayName: user.displayName } + const token = signToken(authUser) + setAuthCookie(res, token) + + return res.json({ user: authUser }) + } catch (err) { + console.error('POST /auth/login failed', err) + return res.status(500).json({ message: 'internal_error' }) + } +}) + +app.post('/auth/register', async (req, res) => { + const parsed = registerSchema.safeParse(req.body) + if (!parsed.success) { + return res.status(400).json({ message: 'invalid_body', issues: parsed.error.issues }) + } + + try { + const db = getDb() + + // Vérifier s'il y a déjà des utilisateurs + const [{ count }] = await db.select({ count: sql`count(*)::int` }).from(users) + const isFirstUser = count === 0 + + // Si ce n'est pas le premier utilisateur, il faut être authentifié + if (!isFirstUser) { + const token = req.cookies?.auth_token + const currentUser = token ? verifyToken(token) : null + if (!currentUser) { + return res.status(401).json({ message: 'auth_required' }) + } + } + + const passwordHash = await bcrypt.hash(parsed.data.password, 12) + + const [created] = await db + .insert(users) + .values({ + username: parsed.data.username, + passwordHash, + displayName: parsed.data.displayName + }) + .returning() + + const authUser: AuthUser = { id: created.id, username: created.username, displayName: created.displayName } + + // Si c'est le premier utilisateur, le connecter automatiquement + if (isFirstUser) { + const token = signToken(authUser) + setAuthCookie(res, token) + } + + return res.status(201).json({ user: authUser }) + } catch (err) { + const maybePgCode = (err as { code?: string } | null)?.code + if (maybePgCode === '23505') { + return res.status(409).json({ message: 'username_taken' }) + } + console.error('POST /auth/register failed', err) + return res.status(500).json({ message: 'internal_error' }) + } +}) + +app.post('/auth/logout', (_req, res) => { + res.clearCookie('auth_token', { path: '/' }) + return res.json({ message: 'logged_out' }) +}) + +app.get('/auth/me', (req, res) => { + const token = req.cookies?.auth_token + if (!token) { + return res.status(401).json({ message: 'not_authenticated' }) + } + + const user = verifyToken(token) + if (!user) { + res.clearCookie('auth_token', { path: '/' }) + return res.status(401).json({ message: 'invalid_token' }) + } + + return res.json({ user }) +}) + +// --- Auth middleware for /admin/* --- + +app.use('/admin', (req, res, next) => { + const token = req.cookies?.auth_token + if (!token) { + return res.status(401).json({ message: 'not_authenticated' }) + } + + const user = verifyToken(token) + if (!user) { + res.clearCookie('auth_token', { path: '/' }) + return res.status(401).json({ message: 'invalid_token' }) + } + + req.user = user + next() +}) + +app.get('/admin/fota/filter-list', async (req, res) => { + const field = String(req.query.field ?? '') + const allowedFields = new Set(['company_id', 'group_id']) + if (!allowedFields.has(field)) { + return res.status(400).json({ message: 'invalid_field' }) + } + + try { + const result = await teltonikaClient.get('/devices/filterList', { + headers: getTeltonikaAuthHeaders(), + params: { field } + }) + + return res.json(result.data) + } catch (err) { + const error = err as any + const message = error?.message ? String(error.message) : 'unknown_error' + const status = error?.response?.status + const data = error?.response?.data + + console.error('GET /admin/fota/filter-list failed', { + message, + status, + data + }) + + if (message.includes('TELTONIKA_FOTA_API_TOKEN is required')) { + return res.status(500).json({ message: 'missing_teltonika_token' }) + } + + if (message.includes('TELTONIKA_FOTA_USER_AGENT is required')) { + return res.status(500).json({ message: 'missing_teltonika_user_agent' }) + } + + if (typeof status === 'number') { + return res.status(502).json({ + message: 'teltonika_error', + status, + data + }) + } + + return res.status(500).json({ message: 'internal_error', detail: message }) + } +}) + +app.get('/admin/fota/lookup', async (req, res) => { + const identifier = String(req.query.identifier ?? '').trim() + if (!identifier) { + return res.status(400).json({ message: 'identifier_required' }) + } + + try { + const resolved = await resolveTeltonikaDeviceByIdentifier(identifier) + const device = resolved.device + return res.json({ + imei: resolved.imei, + fotaModel: device?.model ?? null, + fotaSerial: device?.serial ?? null, + fotaCurrentFirmware: device?.current_firmware ?? null, + fotaActivityStatus: normalizeFotaActivityStatus(device?.activity_status), + fotaSeenAt: device?.seen_at ?? null, + fotaCompanyName: device?.company_name ?? null, + fotaGroupName: device?.group_name ?? null + }) + } catch (err) { + const error = err as any + const message = error?.message ? String(error.message) : 'unknown_error' + const status = error?.response?.status + const data = error?.response?.data + + console.error('GET /admin/fota/lookup failed', { identifier, message, status, data }) + + if (message === 'invalid_identifier' || message === 'invalid_imei') { + return res.status(400).json({ message }) + } + if (message === 'device_not_found') { + return res.status(404).json({ message }) + } + if (message.includes('TELTONIKA_FOTA_API_TOKEN is required')) { + return res.status(500).json({ message: 'missing_teltonika_token' }) + } + if (message.includes('TELTONIKA_FOTA_USER_AGENT is required')) { + return res.status(500).json({ message: 'missing_teltonika_user_agent' }) + } + if (typeof status === 'number') { + return res.status(502).json({ message: 'teltonika_error', status, data }) + } + return res.status(500).json({ message: 'internal_error', detail: message }) + } +}) + +app.get('/admin/sku-mappings', async (_req, res) => { + try { + const db = getDb() + const rows = await db.select().from(skuMappings).orderBy(skuMappings.createdAt) + res.json(rows) + } catch (err) { + // eslint-disable-next-line no-console + console.error('GET /admin/sku-mappings failed', err) + res.status(500).json({ message: 'internal_error' }) + } +}) + +app.post('/admin/sku-mappings', async (req, res) => { + const parsed = skuMappingSchema.safeParse(req.body) + if (!parsed.success) { + return res.status(400).json({ + message: 'invalid_body', + issues: parsed.error.issues + }) + } + + try { + const db = getDb() + + const [created] = await db + .insert(skuMappings) + .values({ + amazonSku: parsed.data.amazonSku, + enterpriseSku: parsed.data.enterpriseSku, + expectedFotaModel: parsed.data.expectedFotaModel, + axonautProductInternalId: parsed.data.axonautProductInternalId + }) + .returning() + + return res.status(201).json(created) + } catch (err) { + // eslint-disable-next-line no-console + console.error('POST /admin/sku-mappings failed', err) + return res.status(500).json({ message: 'internal_error' }) + } +}) + +app.patch('/admin/sku-mappings/:id', async (req, res) => { + const { id } = req.params + const patchSchema = skuMappingSchema.partial() + const parsed = patchSchema.safeParse(req.body) + if (!parsed.success) { + return res.status(400).json({ + message: 'invalid_body', + issues: parsed.error.issues + }) + } + + try { + const db = getDb() + + const [updated] = await db + .update(skuMappings) + .set({ + ...parsed.data, + updatedAt: new Date() + }) + .where(eq(skuMappings.id, id)) + .returning() + + if (!updated) { + return res.status(404).json({ message: 'not_found' }) + } + + return res.json(updated) + } catch (err) { + // eslint-disable-next-line no-console + console.error('PATCH /admin/sku-mappings failed', err) + return res.status(500).json({ message: 'internal_error' }) + } +}) + +app.delete('/admin/sku-mappings/:id', async (req, res) => { + const { id } = req.params + + try { + const db = getDb() + + const deleted = await db + .delete(skuMappings) + .where(eq(skuMappings.id, id)) + .returning({ id: skuMappings.id }) + + if (deleted.length === 0) { + return res.status(404).json({ message: 'not_found' }) + } + + return res.status(204).send() + } catch (err) { + // eslint-disable-next-line no-console + console.error('DELETE /admin/sku-mappings failed', err) + return res.status(500).json({ message: 'internal_error' }) + } +}) + +app.get('/admin/orders', async (_req, res) => { + try { + const db = getDb() + const rows = await db.select().from(orders).orderBy(desc(orders.createdAt)) + res.json(rows) + } catch (err) { + // eslint-disable-next-line no-console + console.error('GET /admin/orders failed', err) + res.status(500).json({ message: 'internal_error' }) + } +}) + +app.post('/admin/orders', async (req, res) => { + const parsed = orderCreateSchema.safeParse(req.body) + if (!parsed.success) { + return res.status(400).json({ + message: 'invalid_body', + issues: parsed.error.issues + }) + } + + try { + const db = getDb() + + const created = await db.transaction(async (tx) => { + const [order] = await tx + .insert(orders) + .values({ + orderRef: parsed.data.orderRef, + status: 'new', + updatedAt: new Date() + }) + .returning() + + await tx.insert(orderItems).values( + parsed.data.items.map((it) => ({ + orderId: order.id, + amazonSku: it.amazonSku, + quantity: it.quantity, + title: it.title ?? null + })) + ) + + return order + }) + + return res.status(201).json({ id: created.id }) + } catch (err) { + // eslint-disable-next-line no-console + console.error('POST /admin/orders failed', err) + return res.status(500).json({ message: 'internal_error' }) + } +}) + +app.get('/admin/orders/:id', async (req, res) => { + const { id } = req.params + + try { + const db = getDb() + + const [order] = await db.select().from(orders).where(eq(orders.id, id)) + if (!order) { + return res.status(404).json({ message: 'not_found' }) + } + + const items = await db + .select() + .from(orderItems) + .where(and(eq(orderItems.orderId, id))) + .orderBy(orderItems.createdAt) + + const skuList = Array.from(new Set(items.map((it) => it.amazonSku))) + const mappings = skuList.length + ? await db + .select({ amazonSku: skuMappings.amazonSku, expectedFotaModel: skuMappings.expectedFotaModel }) + .from(skuMappings) + .where(inArray(skuMappings.amazonSku, skuList)) + : [] + + const expectedModelBySku = new Map( + mappings.map((m) => [m.amazonSku, m.expectedFotaModel ?? null] as const) + ) + + const expectedFotaModels = Array.from( + new Set(mappings.map((m) => (m.expectedFotaModel ?? '').trim()).filter(Boolean)) + ) + + const imeis = await db + .select() + .from(orderImeis) + .where(and(eq(orderImeis.orderId, id))) + .orderBy(orderImeis.createdAt) + + return res.json({ + ...order, + items: items.map((it) => ({ + ...it, + expectedFotaModel: expectedModelBySku.get(it.amazonSku) ?? null + })), + expectedFotaModels, + imeis + }) + } catch (err) { + // eslint-disable-next-line no-console + console.error('GET /admin/orders/:id failed', err) + return res.status(500).json({ message: 'internal_error' }) + } +}) + +app.post('/admin/orders/:id/scan-imei', async (req, res) => { + const { id } = req.params + + const parsed = scanImeiSchema.safeParse(req.body) + if (!parsed.success) { + return res.status(400).json({ + message: 'invalid_body', + issues: parsed.error.issues + }) + } + + const identifier = parsed.data.imei.trim() + + try { + const db = getDb() + + const [order] = await db.select({ id: orders.id }).from(orders).where(eq(orders.id, id)) + if (!order) { + return res.status(404).json({ message: 'not_found' }) + } + + let resolved: { imei: string; device: any } + try { + resolved = await resolveTeltonikaDeviceByIdentifier(identifier) + } catch (err) { + const error = err as any + const message = error?.message ? String(error.message) : 'unknown_error' + const status = error?.response?.status + const data = error?.response?.data + + console.error('resolveTeltonikaDeviceByIdentifier failed', { + message, + status, + data, + identifier + }) + + if (message.includes('TELTONIKA_FOTA_API_TOKEN is required')) { + return res.status(500).json({ message: 'missing_teltonika_token' }) + } + + if (message.includes('TELTONIKA_FOTA_USER_AGENT is required')) { + return res.status(500).json({ message: 'missing_teltonika_user_agent' }) + } + + if (message === 'invalid_identifier' || message === 'invalid_imei') { + return res.status(400).json({ message }) + } + + if (message === 'device_not_found') { + return res.status(404).json({ message }) + } + + if (typeof status === 'number') { + return res.status(502).json({ + message: 'teltonika_error', + status, + data + }) + } + + return res.status(500).json({ message: 'internal_error', detail: message }) + } + + const { imei } = resolved + let device = resolved.device + + const needsDeviceHydration = + !device || + typeof device !== 'object' || + typeof device.model === 'undefined' || + typeof device.serial === 'undefined' || + typeof device.current_firmware === 'undefined' || + typeof device.activity_status === 'undefined' + + if (needsDeviceHydration) { + try { + device = await lookupTeltonikaDeviceByImei(imei) + } catch (err) { + const error = err as any + const message = error?.message ? String(error.message) : 'unknown_error' + const status = error?.response?.status + const data = error?.response?.data + console.error('lookupTeltonikaDeviceByImei failed after resolve', { + message, + status, + data, + imei, + identifier + }) + } + } + + try { + const [created] = await db + .insert(orderImeis) + .values({ + orderId: id, + imei + }) + .returning() + + try { + const activityStatus = normalizeFotaActivityStatus(device?.activity_status) + await db + .update(orderImeis) + .set({ + fotaModel: device?.model ?? null, + fotaSerial: device?.serial ?? null, + fotaCurrentFirmware: device?.current_firmware ?? null, + fotaActivityStatus: activityStatus, + fotaSeenAt: device?.seen_at ?? null, + fotaCompanyId: device?.company_id ?? null, + fotaCompanyName: device?.company_name ?? null, + fotaGroupId: device?.group_id ?? null, + fotaGroupName: device?.group_name ?? null, + fotaLookupError: null, + fotaLastLookupAt: new Date() + }) + .where(eq(orderImeis.id, created.id)) + } catch (err) { + const rawMsg = err instanceof Error ? err.message : String(err) + const msg = rawMsg.includes('Failed query:') ? 'fota_lookup_failed' : rawMsg + try { + await db + .update(orderImeis) + .set({ + fotaLookupError: msg, + fotaLastLookupAt: new Date() + }) + .where(eq(orderImeis.id, created.id)) + } catch (writeErr) { + console.error('Failed to persist fotaLookupError', writeErr) + } + } + + // Relire l'IMEI depuis la DB pour inclure les données FOTA mises à jour + const [fresh] = await db.select().from(orderImeis).where(eq(orderImeis.id, created.id)) + return res.status(201).json(fresh ?? created) + } catch (err) { + const maybePgCode = (err as { code?: string } | null)?.code + if (maybePgCode === '23505') { + return res.status(409).json({ message: 'already_scanned' }) + } + + const error = err as any + const message = error?.message ? String(error.message) : 'unknown_error' + console.error('Insert order imei failed', { + message, + code: error?.code + }) + + return res.status(500).json({ message: 'internal_error', detail: message }) + } + } catch (err) { + const error = err as any + const message = error?.message ? String(error.message) : 'unknown_error' + const status = error?.response?.status + const data = error?.response?.data + + console.error('POST /admin/orders/:id/scan-imei failed', { + message, + status, + data + }) + + if (typeof status === 'number') { + return res.status(502).json({ + message: 'teltonika_error', + status, + data + }) + } + + return res.status(500).json({ message: 'internal_error', detail: message }) + } +}) + +app.post('/admin/orders/:id/fota-move', async (req, res) => { + const { id } = req.params + + const targetCompanyId = toMaybeInt64(process.env.TELTONIKA_FOTA_TARGET_COMPANY_ID ?? '') + const targetGroupId = toMaybeInt64(process.env.TELTONIKA_FOTA_TARGET_GROUP_ID ?? '') + if (targetCompanyId === null || targetGroupId === null) { + return res.status(500).json({ message: 'missing_fota_target_ids' }) + } + + try { + const db = getDb() + + const [order] = await db.select({ id: orders.id }).from(orders).where(eq(orders.id, id)) + if (!order) { + return res.status(404).json({ message: 'not_found' }) + } + + const items = await db + .select({ amazonSku: orderItems.amazonSku, quantity: orderItems.quantity }) + .from(orderItems) + .where(eq(orderItems.orderId, id)) + + const totalExpected = items.reduce((sum, it) => sum + it.quantity, 0) + + const skuList = Array.from(new Set(items.map((it) => it.amazonSku))) + const mappings = skuList.length + ? await db + .select({ amazonSku: skuMappings.amazonSku, expectedFotaModel: skuMappings.expectedFotaModel }) + .from(skuMappings) + .where(inArray(skuMappings.amazonSku, skuList)) + : [] + + const expectedModels = new Set( + mappings.map((m) => (m.expectedFotaModel ?? '').trim()).filter(Boolean) + ) + + const imeis = await db + .select({ + id: orderImeis.id, + imei: orderImeis.imei, + fotaModel: orderImeis.fotaModel, + fotaLookupError: orderImeis.fotaLookupError + }) + .from(orderImeis) + .where(eq(orderImeis.orderId, id)) + .orderBy(orderImeis.createdAt) + + if (imeis.length === 0) { + return res.status(400).json({ message: 'no_imeis' }) + } + + if (imeis.length !== totalExpected) { + return res.status(400).json({ + message: 'imei_count_mismatch', + scanned: imeis.length, + expected: totalExpected + }) + } + + const missingFotaInfo = imeis + .filter((it) => { + const model = (it.fotaModel ?? '').trim() + if (!model) return true + if (it.fotaLookupError) return true + return false + }) + .map((it) => ({ + imei: it.imei, + fotaModel: it.fotaModel ?? null, + fotaLookupError: it.fotaLookupError ?? null + })) + + if (missingFotaInfo.length > 0) { + return res.status(400).json({ + message: 'missing_fota_info', + missing: missingFotaInfo + }) + } + + if (expectedModels.size > 0) { + const mismatches = imeis + .filter((it) => { + const model = (it.fotaModel ?? '').trim() + return !expectedModels.has(model) + }) + .map((it) => ({ + imei: it.imei, + fotaModel: it.fotaModel ?? null + })) + + if (mismatches.length > 0) { + return res.status(400).json({ + message: 'fota_model_mismatch', + expectedModels: Array.from(expectedModels), + mismatches + }) + } + } + + const idList = imeis + .map((x) => toMaybeInt64(x.imei)) + .filter((x): x is number => x !== null) + + if (idList.length === 0) { + return res.status(400).json({ message: 'invalid_imeis' }) + } + + const result = await teltonikaClient.post( + '/devices/bulkUpdate', + { + source: 'id_list', + id_list: idList, + data: { + company_id: targetCompanyId, + group_id: targetGroupId + } + }, + { + headers: getTeltonikaAuthHeaders() + } + ) + + await db + .update(orderImeis) + .set({ + fotaMoveError: null, + fotaMovedAt: new Date() + }) + .where(eq(orderImeis.orderId, id)) + + await db + .update(orders) + .set({ + status: 'ready', + updatedAt: new Date() + }) + .where(eq(orders.id, id)) + + return res.json({ + ...result.data, + targetCompanyId, + targetGroupId, + movedImeisCount: idList.length, + orderStatus: 'ready' + }) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + console.error('POST /admin/orders/:id/fota-move failed', err) + + try { + const db = getDb() + await db + .update(orderImeis) + .set({ + fotaMoveError: msg + }) + .where(eq(orderImeis.orderId, id)) + } catch { + // ignore + } + + return res.status(500).json({ message: 'internal_error' }) + } +}) + +// --- Axonaut --- + +const getAxonautApiKey = async () => { + const db = getDb() + const [row] = await db + .select({ value: apiKeys.value }) + .from(apiKeys) + .where(and(eq(apiKeys.provider, 'axonaut'), eq(apiKeys.label, 'api_key'))) + if (!row) { + throw new Error('axonaut_api_key_not_configured') + } + return row.value +} + +const axonautClient = axios.create({ + baseURL: 'https://axonaut.com/api/v2', + timeout: 15000, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + } +}) + +app.get('/admin/axonaut/products', async (req, res) => { + try { + const apiKey = await getAxonautApiKey() + const params: Record = {} + if (req.query.product_code) params.product_code = String(req.query.product_code) + if (req.query.internal_id) params.internal_id = String(req.query.internal_id) + if (req.query.name) params.name = String(req.query.name) + + const result = await axonautClient.get('/products', { + headers: { userApiKey: apiKey }, + params + }) + return res.json(result.data) + } catch (err) { + const error = err as any + const message = error?.message ? String(error.message) : 'unknown_error' + const status = error?.response?.status + const data = error?.response?.data + console.error('GET /admin/axonaut/products failed', { message, status, data }) + + if (message.includes('axonaut_api_key_not_configured')) { + return res.status(500).json({ message: 'axonaut_api_key_not_configured' }) + } + if (typeof status === 'number') { + return res.status(502).json({ message: 'axonaut_error', status, data }) + } + return res.status(500).json({ message: 'internal_error', detail: message }) + } +}) + +app.post('/admin/orders/:id/axonaut-destock', async (req, res) => { + const { id } = req.params + + try { + const db = getDb() + + const [order] = await db.select().from(orders).where(eq(orders.id, id)) + if (!order) { + return res.status(404).json({ message: 'not_found' }) + } + + if (order.axonautDestockedAt) { + return res.status(409).json({ message: 'already_destocked', destockedAt: order.axonautDestockedAt }) + } + + let apiKey: string + try { + apiKey = await getAxonautApiKey() + } catch { + return res.status(500).json({ message: 'axonaut_api_key_not_configured' }) + } + + const items = await db + .select({ amazonSku: orderItems.amazonSku, quantity: orderItems.quantity, title: orderItems.title }) + .from(orderItems) + .where(eq(orderItems.orderId, id)) + + const skuList = Array.from(new Set(items.map((it) => it.amazonSku))) + const mappings = skuList.length + ? await db + .select({ + amazonSku: skuMappings.amazonSku, + axonautProductInternalId: skuMappings.axonautProductInternalId, + enterpriseSku: skuMappings.enterpriseSku + }) + .from(skuMappings) + .where(inArray(skuMappings.amazonSku, skuList)) + : [] + + const mappingBySku = new Map(mappings.map((m) => [m.amazonSku, m])) + + const missingMappings = skuList.filter((sku) => { + const m = mappingBySku.get(sku) + return !m || !m.axonautProductInternalId.trim() + }) + + if (missingMappings.length > 0) { + return res.status(400).json({ + message: 'missing_axonaut_product_id', + skus: missingMappings + }) + } + + const imeis = await db + .select({ imei: orderImeis.imei, fotaModel: orderImeis.fotaModel }) + .from(orderImeis) + .where(eq(orderImeis.orderId, id)) + .orderBy(orderImeis.createdAt) + + const axonautHeaders = { userApiKey: apiKey } + + // Regrouper les quantités par axonaut product internal_id + const qtyByAxonautId = new Map() + for (const item of items) { + const mapping = mappingBySku.get(item.amazonSku) + if (!mapping) continue + const axonautId = mapping.axonautProductInternalId.trim() + const existing = qtyByAxonautId.get(axonautId) + if (existing) { + existing.quantity += item.quantity + existing.skus.push(item.amazonSku) + } else { + qtyByAxonautId.set(axonautId, { quantity: item.quantity, skus: [item.amazonSku] }) + } + } + + const stockResults: Array<{ + axonautInternalId: string + skus: string[] + previousStock: number + newStock: number + destocked: number + }> = [] + + // Destocker chaque produit Axonaut + for (const [axonautInternalId, { quantity, skus }] of qtyByAxonautId) { + const productId = axonautInternalId + + // Vérifier que le produit existe + try { + const productRes = await axonautClient.get(`/products/${productId}`, { headers: axonautHeaders }) + console.log(`[axonaut] GET /products/${productId} → ${productRes.status}`) + } catch (checkErr: any) { + console.error(`[axonaut] GET /products/${productId} FAILED`, checkErr?.response?.status, checkErr?.response?.data) + if (checkErr?.response?.status === 404) { + await db + .update(orders) + .set({ axonautDestockError: `Produit Axonaut introuvable: id=${productId}`, updatedAt: new Date() }) + .where(eq(orders.id, id)) + return res.status(400).json({ + message: 'axonaut_product_not_found', + axonautInternalId + }) + } + throw checkErr + } + + // Récupérer le stock actuel + const stockRes = await axonautClient.get(`/products/${productId}/stock`, { + headers: axonautHeaders + }) + + const stockData = stockRes.data + console.log(`[axonaut] GET /products/${productId}/stock → `, JSON.stringify(stockData)) + + // Supporter les deux noms de champ possibles + let currentStock: number + if (typeof stockData?.current_stock === 'number') { + currentStock = stockData.current_stock + } else if (typeof stockData?.quantity === 'number') { + currentStock = stockData.quantity + } else if (typeof stockData?.stock === 'number') { + currentStock = stockData.stock + } else { + // Fallback : chercher n'importe quel champ numérique qui ressemble à un stock + console.error(`[axonaut] Impossible de déterminer le stock actuel. Données brutes:`, stockData) + await db + .update(orders) + .set({ axonautDestockError: `Stock Axonaut illisible pour produit ${productId}: ${JSON.stringify(stockData)}`, updatedAt: new Date() }) + .where(eq(orders.id, id)) + return res.status(400).json({ + message: 'axonaut_stock_unreadable', + axonautInternalId, + rawStockData: stockData + }) + } + + const newStock = currentStock - quantity + + // Mettre à jour le stock (valeur absolue) — Axonaut attend "stock" dans le PATCH + const patchBody = { stock: newStock } + console.log(`[axonaut] PATCH /products/${productId}/stock body:`, JSON.stringify(patchBody)) + + try { + const patchRes = await axonautClient.patch(`/products/${productId}/stock`, patchBody, { + headers: axonautHeaders + }) + console.log(`[axonaut] PATCH /products/${productId}/stock → ${patchRes.status}`, JSON.stringify(patchRes.data)) + } catch (patchErr: any) { + console.error(`[axonaut] PATCH /products/${productId}/stock FAILED`, + patchErr?.response?.status, JSON.stringify(patchErr?.response?.data)) + throw patchErr + } + + stockResults.push({ + axonautInternalId, + skus, + previousStock: currentStock, + newStock, + destocked: quantity + }) + } + + // Créer la note avec les IMEI (non-bloquant : si ça échoue, le destockage reste valide) + let noteCreated = false + try { + const imeiLines = imeis.map((it) => { + const model = it.fotaModel ? ` (${it.fotaModel})` : '' + return `- ${it.imei}${model}` + }).join('\n') + + const stockSummary = stockResults.map((r) => { + return `${r.skus.join(', ')}: ${r.previousStock} → ${r.newStock} (-${r.destocked})` + }).join('\n') + + const noteContent = [ + `Destockage commande Amazon #${order.orderRef}`, + '', + 'Stock modifié :', + stockSummary, + '', + `IMEI (${imeis.length}) :`, + imeiLines + ].join('\n') + + // company_id : body override > société Amazon par défaut + const AXONAUT_AMAZON_COMPANY_ID = 45114290 + const companyId = req.body?.axonaut_company_id + ? Number(req.body.axonaut_company_id) + : AXONAUT_AMAZON_COMPANY_ID + + const eventPayload: Record = { + company_id: companyId, + title: `Vente Amazon - ${order.orderRef}`, + content: noteContent, + nature: 6, + date: new Date().toISOString(), + is_done: true + } + + console.log(`[axonaut] POST /events body:`, JSON.stringify(eventPayload)) + const eventRes = await axonautClient.post('/events', eventPayload, { + headers: axonautHeaders + }) + console.log(`[axonaut] POST /events → ${eventRes.status}`) + noteCreated = true + } catch (eventErr: any) { + console.error(`[axonaut] POST /events FAILED (non-bloquant)`, + eventErr?.response?.status, JSON.stringify(eventErr?.response?.data)) + } + + // Marquer la commande comme destockée + await db + .update(orders) + .set({ + axonautDestockedAt: new Date(), + axonautDestockError: null, + updatedAt: new Date() + }) + .where(eq(orders.id, id)) + + return res.json({ + message: 'destocked', + stockResults, + imeiCount: imeis.length, + noteCreated + }) + } catch (err) { + const error = err as any + const message = error?.message ? String(error.message) : 'unknown_error' + const status = error?.response?.status + const data = error?.response?.data + + console.error('POST /admin/orders/:id/axonaut-destock failed', { + message, + status, + data + }) + + try { + const db = getDb() + await db + .update(orders) + .set({ axonautDestockError: message, updatedAt: new Date() }) + .where(eq(orders.id, id)) + } catch { + // ignore + } + + if (typeof status === 'number') { + return res.status(502).json({ + message: 'axonaut_error', + status, + data + }) + } + + return res.status(500).json({ message: 'internal_error', detail: message }) + } +}) + +app.delete('/admin/orders/:orderId/imeis/:imeiId', async (req, res) => { + const { orderId, imeiId } = req.params + + try { + const db = getDb() + + const deleted = await db + .delete(orderImeis) + .where(and(eq(orderImeis.id, imeiId), eq(orderImeis.orderId, orderId))) + .returning({ id: orderImeis.id }) + + if (deleted.length === 0) { + return res.status(404).json({ message: 'not_found' }) + } + + return res.status(204).send() + } catch (err) { + // eslint-disable-next-line no-console + console.error('DELETE /admin/orders/:orderId/imeis/:imeiId failed', err) + return res.status(500).json({ message: 'internal_error' }) + } +}) + +// --- API Keys --- + +const maskValue = (value: string) => { + if (value.length <= 4) return '****' + return '****' + value.slice(-4) +} + +app.get('/admin/api-keys', async (_req, res) => { + try { + const db = getDb() + const rows = await db.select().from(apiKeys).orderBy(apiKeys.provider, apiKeys.label) + const masked = rows.map((row) => ({ + ...row, + value: maskValue(row.value) + })) + res.json(masked) + } catch (err) { + console.error('GET /admin/api-keys failed', err) + res.status(500).json({ message: 'internal_error' }) + } +}) + +app.post('/admin/api-keys', async (req, res) => { + const parsed = apiKeyCreateSchema.safeParse(req.body) + if (!parsed.success) { + return res.status(400).json({ + message: 'invalid_body', + issues: parsed.error.issues + }) + } + + try { + const db = getDb() + + const [created] = await db + .insert(apiKeys) + .values({ + provider: parsed.data.provider, + label: parsed.data.label, + value: parsed.data.value + }) + .returning() + + return res.status(201).json({ + ...created, + value: maskValue(created.value) + }) + } catch (err) { + const maybePgCode = (err as { code?: string } | null)?.code + if (maybePgCode === '23505') { + return res.status(409).json({ message: 'duplicate_key' }) + } + console.error('POST /admin/api-keys failed', err) + return res.status(500).json({ message: 'internal_error' }) + } +}) + +app.patch('/admin/api-keys/:id', async (req, res) => { + const { id } = req.params + const parsed = apiKeyUpdateSchema.safeParse(req.body) + if (!parsed.success) { + return res.status(400).json({ + message: 'invalid_body', + issues: parsed.error.issues + }) + } + + try { + const db = getDb() + + const [updated] = await db + .update(apiKeys) + .set({ + value: parsed.data.value, + updatedAt: new Date() + }) + .where(eq(apiKeys.id, id)) + .returning() + + if (!updated) { + return res.status(404).json({ message: 'not_found' }) + } + + return res.json({ + ...updated, + value: maskValue(updated.value) + }) + } catch (err) { + console.error('PATCH /admin/api-keys failed', err) + return res.status(500).json({ message: 'internal_error' }) + } +}) + +app.delete('/admin/api-keys/:id', async (req, res) => { + const { id } = req.params + + try { + const db = getDb() + + const deleted = await db + .delete(apiKeys) + .where(eq(apiKeys.id, id)) + .returning({ id: apiKeys.id }) + + if (deleted.length === 0) { + return res.status(404).json({ message: 'not_found' }) + } + + return res.status(204).send() + } catch (err) { + console.error('DELETE /admin/api-keys failed', err) + return res.status(500).json({ message: 'internal_error' }) + } +}) + +// --- Users --- + +app.get('/admin/users', async (_req, res) => { + try { + const db = getDb() + const rows = await db + .select({ + id: users.id, + username: users.username, + displayName: users.displayName, + createdAt: users.createdAt + }) + .from(users) + .orderBy(users.createdAt) + return res.json(rows) + } catch (err) { + console.error('GET /admin/users failed', err) + return res.status(500).json({ message: 'internal_error' }) + } +}) + +app.post('/admin/users', async (req, res) => { + const parsed = registerSchema.safeParse(req.body) + if (!parsed.success) { + return res.status(400).json({ message: 'invalid_body', issues: parsed.error.issues }) + } + + try { + const db = getDb() + const passwordHash = await bcrypt.hash(parsed.data.password, 12) + + const [created] = await db + .insert(users) + .values({ + username: parsed.data.username, + passwordHash, + displayName: parsed.data.displayName + }) + .returning({ + id: users.id, + username: users.username, + displayName: users.displayName, + createdAt: users.createdAt + }) + + return res.status(201).json(created) + } catch (err) { + const maybePgCode = (err as { code?: string } | null)?.code + if (maybePgCode === '23505') { + return res.status(409).json({ message: 'username_taken' }) + } + console.error('POST /admin/users failed', err) + return res.status(500).json({ message: 'internal_error' }) + } +}) + +app.patch('/admin/users/:id/password', async (req, res) => { + const { id } = req.params + const parsed = changePasswordSchema.safeParse(req.body) + if (!parsed.success) { + return res.status(400).json({ message: 'invalid_body', issues: parsed.error.issues }) + } + + try { + const db = getDb() + const passwordHash = await bcrypt.hash(parsed.data.password, 12) + + const [updated] = await db + .update(users) + .set({ passwordHash, updatedAt: new Date() }) + .where(eq(users.id, id)) + .returning({ id: users.id }) + + if (!updated) { + return res.status(404).json({ message: 'not_found' }) + } + + return res.json({ message: 'password_changed' }) + } catch (err) { + console.error('PATCH /admin/users/:id/password failed', err) + return res.status(500).json({ message: 'internal_error' }) + } +}) + +app.delete('/admin/users/:id', async (req, res) => { + const { id } = req.params + + if (req.user?.id === id) { + return res.status(400).json({ message: 'cannot_delete_self' }) + } + + try { + const db = getDb() + + const deleted = await db + .delete(users) + .where(eq(users.id, id)) + .returning({ id: users.id }) + + if (deleted.length === 0) { + return res.status(404).json({ message: 'not_found' }) + } + + return res.status(204).send() + } catch (err) { + console.error('DELETE /admin/users/:id failed', err) + return res.status(500).json({ message: 'internal_error' }) + } +}) + +// --- Amazon SP-API --- + +const getAmazonCredentials = async () => { + const db = getDb() + const rows = await db + .select({ label: apiKeys.label, value: apiKeys.value }) + .from(apiKeys) + .where(eq(apiKeys.provider, 'amazon')) + + const map = new Map(rows.map((r) => [r.label, r.value])) + const clientId = map.get('client_id') + const clientSecret = map.get('client_secret') + const refreshToken = map.get('refresh_token') + + if (!clientId || !clientSecret || !refreshToken) { + const missing = ['client_id', 'client_secret', 'refresh_token'].filter((k) => !map.get(k)) + throw new Error(`amazon_credentials_missing: ${missing.join(', ')}`) + } + + return { clientId, clientSecret, refreshToken } +} + +const AMAZON_MARKETPLACE_FR = 'A13V1IB3VIYZZH' + +const createAmazonClient = async () => { + const { clientId, clientSecret, refreshToken } = await getAmazonCredentials() + const { SellingPartner } = await import('amazon-sp-api') + + return new SellingPartner({ + region: 'eu', + refresh_token: refreshToken, + credentials: { + SELLING_PARTNER_APP_CLIENT_ID: clientId, + SELLING_PARTNER_APP_CLIENT_SECRET: clientSecret + }, + options: { + auto_request_throttled: true + } + }) +} + +// --- Amazon shipping address via RDT --- + +const splitAmazonName = (name: string): { firstName: string; lastName: string } => { + const parts = name.trim().split(/\s+/) + if (parts.length <= 1) { + return { firstName: '', lastName: parts[0] ?? '' } + } + return { firstName: parts[0], lastName: parts.slice(1).join(' ') } +} + +const fetchAmazonShippingAddress = async (sp: any, amazonOrderId: string) => { + try { + const rdtRes: any = await sp.callAPI({ + operation: 'createRestrictedDataToken', + endpoint: 'tokens', + body: { + restrictedResources: [{ + method: 'GET', + path: `/orders/v0/orders/${amazonOrderId}/address`, + dataElements: ['shippingAddress'] + }] + } + }) + + const rdt = rdtRes?.restrictedDataToken ?? rdtRes?.payload?.restrictedDataToken + if (!rdt) { + console.error(`[amazon-address] No RDT returned for ${amazonOrderId}`) + return null + } + + const addrRes: any = await sp.callAPI({ + operation: 'getOrderAddress', + endpoint: 'orders', + path: { orderId: amazonOrderId }, + restricted_data_token: rdt + }) + + const address = addrRes?.payload?.ShippingAddress ?? addrRes?.ShippingAddress + return address ?? null + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + console.error(`[amazon-address] Failed for ${amazonOrderId}:`, msg) + return null + } +} + +const fetchAndStoreShippingAddress = async (sp: any, dbOrderId: string, amazonOrderId: string): Promise => { + try { + const address = await fetchAmazonShippingAddress(sp, amazonOrderId) + + const db = getDb() + + if (!address || !address.Name) { + await db + .update(orders) + .set({ shippingFetchError: 'no_address_returned', updatedAt: new Date() }) + .where(eq(orders.id, dbOrderId)) + return false + } + + const { firstName, lastName } = splitAmazonName(address.Name) + + await db + .update(orders) + .set({ + shippingName: address.Name, + shippingFirstName: firstName, + shippingLastName: lastName, + shippingLine1: address.AddressLine1 ?? null, + shippingLine2: address.AddressLine2 ?? null, + shippingLine3: address.AddressLine3 ?? null, + shippingCity: address.City ?? null, + shippingZipCode: address.PostalCode ?? null, + shippingCountryCode: address.CountryCode ?? address.Country ?? null, + shippingState: address.StateOrRegion ?? null, + shippingPhone: address.Phone ?? null, + shippingAddressType: address.AddressType ?? null, + shippingFetchedAt: new Date(), + shippingFetchError: null, + updatedAt: new Date() + }) + .where(eq(orders.id, dbOrderId)) + + return true + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + console.error(`[amazon-address] Store failed for order ${dbOrderId}:`, msg) + + try { + const db = getDb() + await db + .update(orders) + .set({ shippingFetchError: msg, updatedAt: new Date() }) + .where(eq(orders.id, dbOrderId)) + } catch { + // ignore + } + + return false + } +} + +// Test de connexion Amazon SP-API +app.get('/admin/amazon/test', async (req, res) => { + try { + // Étape 1 : vérifier les credentials + const { clientId, clientSecret, refreshToken } = await getAmazonCredentials() + + // Étape 2 : tester le token LWA (obtenir un access_token) + let accessTokenOk = false + let lwaError: string | null = null + try { + const tokenRes = await axios.post('https://api.amazon.com/auth/o2/token', new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refreshToken, + client_id: clientId, + client_secret: clientSecret + }).toString(), { + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + timeout: 10000 + }) + accessTokenOk = !!tokenRes.data?.access_token + } catch (err) { + const e = err as any + lwaError = e?.response?.data?.error_description ?? e?.response?.data?.error ?? e?.message ?? 'unknown' + console.error('LWA token test failed', { status: e?.response?.status, data: e?.response?.data }) + } + + if (!accessTokenOk) { + return res.status(401).json({ + connected: false, + step: 'lwa_token', + message: 'lwa_authentication_failed', + detail: lwaError + }) + } + + // Étape 3 : tester l'appel SP-API + const sp = await createAmazonClient() + const response: any = await sp.callAPI({ + operation: 'getOrders', + endpoint: 'orders', + query: { + MarketplaceIds: [AMAZON_MARKETPLACE_FR], + CreatedAfter: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString() + } + }) + + const orderCount = (response?.Orders ?? response?.payload?.Orders ?? []).length + + return res.json({ + connected: true, + marketplace: 'Amazon.fr (A13V1IB3VIYZZH)', + ordersLast24h: orderCount + }) + } catch (err) { + const error = err as any + const message = error?.message ? String(error.message) : 'unknown_error' + const code = error?.code ?? error?.statusCode + const body = error?.response?.data ?? error?.details + console.error('GET /admin/amazon/test failed', { message, code, body, keys: Object.keys(error ?? {}) }) + + if (message.includes('amazon_credentials_missing')) { + return res.status(500).json({ connected: false, step: 'credentials', message }) + } + + return res.status(502).json({ + connected: false, + step: 'sp_api_call', + message: 'amazon_api_error', + detail: message, + code, + body + }) + } +}) + +// Liste les commandes Amazon (Unshipped par défaut) +app.get('/admin/amazon/orders', async (req, res) => { + try { + const sp = await createAmazonClient() + + const daysBack = Number(req.query.days) || 7 + const createdAfter = new Date(Date.now() - daysBack * 24 * 60 * 60 * 1000).toISOString() + + const statusParam = req.query.statuses + const orderStatuses = typeof statusParam === 'string' && statusParam.trim() + ? statusParam.split(',').map((s) => s.trim()) + : ['Unshipped', 'PartiallyShipped'] + + const response: any = await sp.callAPI({ + operation: 'getOrders', + endpoint: 'orders', + query: { + MarketplaceIds: [AMAZON_MARKETPLACE_FR], + CreatedAfter: createdAfter, + OrderStatuses: orderStatuses + } + }) + + const amazonOrders = response?.Orders ?? response?.payload?.Orders ?? [] + + return res.json({ orders: amazonOrders, count: amazonOrders.length }) + } catch (err) { + const error = err as any + const message = error?.message ? String(error.message) : 'unknown_error' + console.error('GET /admin/amazon/orders failed', { message, status: error?.statusCode, details: error?.details }) + + if (message.includes('amazon_credentials_missing')) { + return res.status(500).json({ message }) + } + + return res.status(502).json({ message: 'amazon_api_error', detail: message }) + } +}) + +// Récupère les articles d'une commande Amazon +app.get('/admin/amazon/orders/:amazonOrderId/items', async (req, res) => { + try { + const sp = await createAmazonClient() + + const response = await sp.callAPI({ + operation: 'getOrderItems', + endpoint: 'orders', + path: { orderId: req.params.amazonOrderId } + }) + + const items = response?.OrderItems ?? response?.payload?.OrderItems ?? [] + + return res.json({ items }) + } catch (err) { + const error = err as any + const message = error?.message ? String(error.message) : 'unknown_error' + console.error('GET /admin/amazon/orders/:id/items failed', { message }) + + return res.status(502).json({ message: 'amazon_api_error', detail: message }) + } +}) + +// Importe une commande Amazon dans notre base +app.post('/admin/amazon/import', async (req, res) => { + const { amazonOrderId } = req.body ?? {} + + if (!amazonOrderId || typeof amazonOrderId !== 'string') { + return res.status(400).json({ message: 'missing_amazon_order_id' }) + } + + try { + const db = getDb() + + // Vérifier si la commande existe déjà + const [existing] = await db + .select({ id: orders.id }) + .from(orders) + .where(eq(orders.orderRef, amazonOrderId)) + + if (existing) { + return res.status(409).json({ message: 'order_already_exists', orderId: existing.id }) + } + + // Récupérer les articles depuis Amazon + const sp = await createAmazonClient() + + const itemsResponse = await sp.callAPI({ + operation: 'getOrderItems', + endpoint: 'orders', + path: { orderId: amazonOrderId } + }) + + const amazonItems = itemsResponse?.OrderItems ?? itemsResponse?.payload?.OrderItems ?? [] + + if (amazonItems.length === 0) { + return res.status(400).json({ message: 'no_items_found' }) + } + + // Créer la commande + const [newOrder] = await db + .insert(orders) + .values({ + orderRef: amazonOrderId, + status: 'new' + }) + .returning() + + // Créer les articles + const itemValues = amazonItems.map((item: any) => ({ + orderId: newOrder.id, + amazonSku: item.SellerSKU ?? 'UNKNOWN', + amazonOrderItemId: item.OrderItemId ?? null, + quantity: item.QuantityOrdered ?? 1, + title: item.Title ?? null + })) + + await db.insert(orderItems).values(itemValues) + + // Récupérer l'adresse de livraison via RDT + let addressFetched = false + try { + addressFetched = await fetchAndStoreShippingAddress(sp, newOrder.id, amazonOrderId) + } catch (err) { + console.error(`[amazon-import] Address fetch failed for ${amazonOrderId}:`, err) + } + + return res.status(201).json({ + message: 'imported', + orderId: newOrder.id, + orderRef: amazonOrderId, + itemCount: itemValues.length, + addressFetched + }) + } catch (err) { + const error = err as any + const message = error?.message ? String(error.message) : 'unknown_error' + console.error('POST /admin/amazon/import failed', { message }) + + if (message.includes('amazon_credentials_missing')) { + return res.status(500).json({ message }) + } + + return res.status(502).json({ message: 'amazon_api_error', detail: message }) + } +}) + +// Associer un numéro de suivi Colissimo manuellement +app.post('/admin/orders/:id/tracking', async (req, res) => { + const { id } = req.params + const { trackingNumber } = req.body ?? {} + + if (!trackingNumber || typeof trackingNumber !== 'string' || !trackingNumber.trim()) { + return res.status(400).json({ message: 'missing_tracking_number' }) + } + + try { + const db = getDb() + + const [order] = await db.select({ id: orders.id }).from(orders).where(eq(orders.id, id)) + if (!order) { + return res.status(404).json({ message: 'not_found' }) + } + + await db + .update(orders) + .set({ + colissimoTrackingNumber: trackingNumber.trim(), + colissimoShippedAt: new Date(), + colissimoError: null, + status: 'shipped', + updatedAt: new Date() + }) + .where(eq(orders.id, id)) + + return res.json({ message: 'tracking_saved', trackingNumber: trackingNumber.trim() }) + } catch (err) { + const error = err as any + const message = error?.message ? String(error.message) : 'unknown_error' + console.error('POST /admin/orders/:id/tracking failed', { message }) + return res.status(500).json({ message: 'internal_error', detail: message }) + } +}) + +app.delete('/admin/orders/:id/tracking', async (req, res) => { + const { id } = req.params + + try { + const db = getDb() + + const [order] = await db + .select({ + id: orders.id, + colissimoTrackingNumber: orders.colissimoTrackingNumber, + amazonTrackingConfirmedAt: orders.amazonTrackingConfirmedAt + }) + .from(orders) + .where(eq(orders.id, id)) + + if (!order) { + return res.status(404).json({ message: 'not_found' }) + } + + if (!order.colissimoTrackingNumber) { + return res.status(400).json({ message: 'no_tracking_to_delete' }) + } + + if (order.amazonTrackingConfirmedAt) { + return res.status(409).json({ message: 'tracking_already_confirmed_on_amazon' }) + } + + await db + .update(orders) + .set({ + colissimoTrackingNumber: null, + colissimoShippedAt: null, + colissimoLabelUrl: null, + colissimoError: null, + status: 'ready', + updatedAt: new Date() + }) + .where(eq(orders.id, id)) + + return res.json({ message: 'tracking_deleted' }) + } catch (err) { + const error = err as any + const message = error?.message ? String(error.message) : 'unknown_error' + console.error('DELETE /admin/orders/:id/tracking failed', { message }) + return res.status(500).json({ message: 'internal_error', detail: message }) + } +}) + +// Récupérer manuellement l'adresse Amazon pour une commande +app.post('/admin/orders/:id/fetch-address', async (req, res) => { + const { id } = req.params + + try { + const db = getDb() + + const [order] = await db.select({ id: orders.id, orderRef: orders.orderRef }).from(orders).where(eq(orders.id, id)) + if (!order) { + return res.status(404).json({ message: 'not_found' }) + } + + let sp: any + try { + sp = await createAmazonClient() + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + return res.status(500).json({ message: msg }) + } + + const success = await fetchAndStoreShippingAddress(sp, order.id, order.orderRef) + + if (!success) { + return res.status(502).json({ message: 'address_fetch_failed' }) + } + + // Relire la commande pour renvoyer l'adresse + const [updated] = await db.select().from(orders).where(eq(orders.id, id)) + + return res.json({ + message: 'address_fetched', + shippingName: updated?.shippingName ?? null, + shippingFirstName: updated?.shippingFirstName ?? null, + shippingLastName: updated?.shippingLastName ?? null, + shippingLine1: updated?.shippingLine1 ?? null, + shippingLine2: updated?.shippingLine2 ?? null, + shippingLine3: updated?.shippingLine3 ?? null, + shippingCity: updated?.shippingCity ?? null, + shippingZipCode: updated?.shippingZipCode ?? null, + shippingCountryCode: updated?.shippingCountryCode ?? null, + shippingPhone: updated?.shippingPhone ?? null + }) + } catch (err) { + const error = err as any + const message = error?.message ? String(error.message) : 'unknown_error' + console.error('POST /admin/orders/:id/fetch-address failed', { message }) + return res.status(500).json({ message: 'internal_error', detail: message }) + } +}) + +// Confirmer l'expédition sur Amazon (transmet le tracking Colissimo) +app.post('/admin/orders/:id/amazon-confirm-shipment', async (req, res) => { + const { id } = req.params + + try { + const db = getDb() + + const [order] = await db.select().from(orders).where(eq(orders.id, id)) + if (!order) { + return res.status(404).json({ message: 'not_found' }) + } + + if (!order.colissimoTrackingNumber) { + return res.status(400).json({ message: 'no_tracking_number', detail: 'Générez d\'abord une étiquette Colissimo' }) + } + + if (order.amazonTrackingConfirmedAt) { + return res.status(409).json({ + message: 'already_confirmed', + confirmedAt: order.amazonTrackingConfirmedAt + }) + } + + // Récupérer les articles avec leurs OrderItemId Amazon + const items = await db + .select({ + amazonOrderItemId: orderItems.amazonOrderItemId, + quantity: orderItems.quantity + }) + .from(orderItems) + .where(eq(orderItems.orderId, id)) + + const orderItemsForAmazon = items + .filter((it) => it.amazonOrderItemId) + .map((it) => ({ + orderItemId: it.amazonOrderItemId!, + quantity: it.quantity + })) + + if (orderItemsForAmazon.length === 0) { + return res.status(400).json({ + message: 'no_amazon_order_item_ids', + detail: 'Les articles n\'ont pas d\'OrderItemId Amazon. Réimportez la commande.' + }) + } + + const sp = await createAmazonClient() + + await sp.callAPI({ + operation: 'confirmShipment', + endpoint: 'orders', + path: { orderId: order.orderRef }, + body: { + marketplaceId: AMAZON_MARKETPLACE_FR, + packageDetail: { + packageReferenceId: '1', + carrierCode: 'Colissimo', + carrierName: 'Colissimo', + shippingMethod: 'Standard', + trackingNumber: order.colissimoTrackingNumber, + shipDate: (order.colissimoShippedAt ?? new Date()).toISOString(), + orderItems: orderItemsForAmazon + } + } + }) + + await db + .update(orders) + .set({ + amazonTrackingConfirmedAt: new Date(), + amazonTrackingError: null, + status: 'done', + updatedAt: new Date() + }) + .where(eq(orders.id, id)) + + return res.json({ message: 'shipment_confirmed', trackingNumber: order.colissimoTrackingNumber }) + } catch (err) { + const error = err as any + const message = error?.message ? String(error.message) : 'unknown_error' + console.error('POST /admin/orders/:id/amazon-confirm-shipment failed', { message }) + + try { + const db = getDb() + await db + .update(orders) + .set({ amazonTrackingError: message, updatedAt: new Date() }) + .where(eq(orders.id, id)) + } catch { + // ignore + } + + if (message.includes('amazon_credentials_missing')) { + return res.status(500).json({ message }) + } + + return res.status(502).json({ message: 'amazon_confirm_error', detail: message }) + } +}) + +// --- Colissimo --- + +const COLISSIMO_BASE_URL = 'https://ws.colissimo.fr/sls-ws/SlsServiceWSRest/3.1' + +const getColissimoCredentials = async () => { + const db = getDb() + const rows = await db + .select({ label: apiKeys.label, value: apiKeys.value }) + .from(apiKeys) + .where(eq(apiKeys.provider, 'laposte')) + + const map = new Map(rows.map((r) => [r.label, r.value])) + const contractNumber = map.get('contract_number') + const password = map.get('password') + + if (!contractNumber || !password) { + const missing = ['contract_number', 'password'].filter((k) => !map.get(k)) + throw new Error(`colissimo_credentials_missing: ${missing.join(', ')}`) + } + + return { contractNumber, password } +} + +// Parse une réponse MTOM multipart pour extraire le JSON et le PDF +const parseColissimoMtomResponse = (buffer: Buffer, contentType: string) => { + const boundaryMatch = contentType.match(/boundary="?([^";]+)"?/) + if (!boundaryMatch) { + // Peut-être une réponse JSON simple (erreur) + const text = buffer.toString('utf-8') + try { + return { json: JSON.parse(text), pdf: null } + } catch { + throw new Error('colissimo_invalid_response: no boundary and not JSON') + } + } + + const boundary = boundaryMatch[1] + const raw = buffer.toString('binary') + const parts = raw.split('--' + boundary).filter((p) => p.trim() && p.trim() !== '--') + + let json: any = null + let pdf: Buffer | null = null + + for (const part of parts) { + const headerEnd = part.indexOf('\r\n\r\n') + if (headerEnd === -1) continue + + const headers = part.substring(0, headerEnd).toLowerCase() + const body = part.substring(headerEnd + 4) + + // Retirer le trailing \r\n + const cleanBody = body.replace(/\r\n$/, '') + + if (headers.includes('application/json')) { + try { + json = JSON.parse(cleanBody) + } catch { + // ignore + } + } else if (headers.includes('application/pdf') || headers.includes('application/octet-stream')) { + pdf = Buffer.from(cleanBody, 'binary') + } + } + + return { json, pdf } +} + +// Adresse expéditeur par défaut (à configurer selon votre entrepôt) +const SENDER_ADDRESS = { + companyName: 'Géolock', + line2: '13 allée des cabanes', + countryCode: 'FR', + city: 'Gujan-Mestras', + zipCode: '33470' +} + +// Génère une étiquette Colissimo pour une commande +app.post('/admin/orders/:id/colissimo-label', async (req, res) => { + const { id } = req.params + + try { + const db = getDb() + + const [order] = await db.select().from(orders).where(eq(orders.id, id)) + if (!order) { + return res.status(404).json({ message: 'not_found' }) + } + + if (order.colissimoTrackingNumber) { + return res.status(409).json({ + message: 'label_already_generated', + trackingNumber: order.colissimoTrackingNumber + }) + } + + const { contractNumber, password } = await getColissimoCredentials() + + // Données du destinataire : body prioritaire, fallback sur adresse stockée en DB + const body = req.body ?? {} + const lastName = body.lastName || order.shippingLastName || '' + const firstName = body.firstName || order.shippingFirstName || '' + const line2 = body.line2 || order.shippingLine1 || '' + const line3 = body.line3 || order.shippingLine2 || undefined + const countryCode = body.countryCode || order.shippingCountryCode || 'FR' + const city = body.city || order.shippingCity || '' + const zipCode = body.zipCode || order.shippingZipCode || '' + const weight = body.weight ?? 0.5 + const productCode = body.productCode ?? 'DOM' + + if (!lastName || !line2 || !city || !zipCode) { + return res.status(400).json({ + message: 'missing_address_fields', + required: ['lastName', 'line2', 'city', 'zipCode'] + }) + } + + const depositDate = new Date().toISOString().split('T')[0] + + const colissimoPayload = { + contractNumber, + password, + outputFormat: { + x: 0, + y: 0, + outputPrintingType: 'PDF_10x15_300dpi' + }, + letter: { + service: { + productCode, + depositDate, + orderNumber: order.orderRef, + commercialName: 'Localiztoi' + }, + parcel: { + weight: Number(weight) + }, + sender: { + address: SENDER_ADDRESS + }, + addressee: { + address: { + lastName, + firstName: firstName ?? '', + line2, + ...(line3 ? { line3 } : {}), + countryCode, + city, + zipCode + } + } + } + } + + const response = await axios.post( + `${COLISSIMO_BASE_URL}/generateLabel`, + colissimoPayload, + { + headers: { 'Content-Type': 'application/json', Accept: '*/*' }, + responseType: 'arraybuffer', + timeout: 30000, + validateStatus: () => true + } + ) + + // Si erreur HTTP brute de Colissimo + if (response.status >= 400) { + const bodyText = Buffer.from(response.data).toString('utf-8').substring(0, 2000) + let parsed: any = null + try { parsed = JSON.parse(bodyText) } catch { /* ignore */ } + const errorDetail = parsed?.messages?.[0]?.messageContent ?? parsed?.message ?? bodyText + + await db + .update(orders) + .set({ colissimoError: `HTTP ${response.status}: ${errorDetail}`, updatedAt: new Date() }) + .where(eq(orders.id, id)) + + return res.status(response.status).json({ + message: 'colissimo_label_error', + httpStatus: response.status, + detail: parsed ?? bodyText + }) + } + + const responseContentType = response.headers['content-type'] ?? '' + const { json: labelJson, pdf } = parseColissimoMtomResponse( + Buffer.from(response.data), + responseContentType + ) + + // Vérifier les erreurs + const messages = labelJson?.messages ?? [] + const errorMsg = messages.find((m: any) => m.type === 'ERROR') + if (errorMsg) { + await db + .update(orders) + .set({ colissimoError: `${errorMsg.id}: ${errorMsg.messageContent ?? errorMsg.id}`, updatedAt: new Date() }) + .where(eq(orders.id, id)) + + return res.status(400).json({ + message: 'colissimo_label_error', + error: errorMsg + }) + } + + const trackingNumber = labelJson?.labelResponse?.parcelNumber ?? null + + // Sauvegarder le PDF sur le disque + let labelUrl: string | null = null + if (pdf && trackingNumber) { + const fs = await import('fs/promises') + const path = await import('path') + const labelsDir = path.join(process.cwd(), 'labels') + await fs.mkdir(labelsDir, { recursive: true }) + const filename = `${trackingNumber}.pdf` + await fs.writeFile(path.join(labelsDir, filename), pdf) + labelUrl = `/labels/${filename}` + } + + // Mettre à jour la commande + await db + .update(orders) + .set({ + colissimoTrackingNumber: trackingNumber, + colissimoLabelUrl: labelUrl, + colissimoShippedAt: new Date(), + colissimoError: null, + status: 'shipped', + updatedAt: new Date() + }) + .where(eq(orders.id, id)) + + return res.json({ + message: 'label_generated', + trackingNumber, + labelUrl + }) + } catch (err) { + const error = err as any + const message = error?.message ? String(error.message) : 'unknown_error' + console.error('POST /admin/orders/:id/colissimo-label failed', { message, status: error?.response?.status, data: error?.response?.data?.toString?.()?.substring(0, 500) }) + + if (message.includes('colissimo_credentials_missing')) { + return res.status(500).json({ message }) + } + + try { + const db = getDb() + await db + .update(orders) + .set({ colissimoError: message, updatedAt: new Date() }) + .where(eq(orders.id, id)) + } catch { + // ignore + } + + return res.status(502).json({ message: 'colissimo_error', detail: message }) + } +}) + +// Télécharger l'étiquette PDF +app.get('/labels/:filename', async (req, res) => { + try { + const fs = await import('fs/promises') + const path = await import('path') + + // Sécurité : empêcher path traversal + const filename = path.basename(req.params.filename) + if (!filename || filename !== req.params.filename || filename.includes('..')) { + return res.status(400).json({ message: 'invalid_filename' }) + } + + const labelsDir = path.resolve(process.cwd(), 'labels') + const filePath = path.join(labelsDir, filename) + + // Vérifier que le chemin résolu est bien dans le dossier labels + if (!filePath.startsWith(labelsDir + path.sep)) { + return res.status(400).json({ message: 'invalid_filename' }) + } + + const data = await fs.readFile(filePath) + res.setHeader('Content-Type', 'application/pdf') + res.setHeader('Content-Disposition', `inline; filename="${filename}"`) + return res.send(data) + } catch { + return res.status(404).json({ message: 'label_not_found' }) + } +}) + +// Test connexion Colissimo +app.get('/admin/colissimo/test', async (req, res) => { + try { + const { contractNumber, password } = await getColissimoCredentials() + + // checkGenerateLabel = test sans générer de vrai colis + const testPayload = { + contractNumber, + password, + outputFormat: { x: 0, y: 0, outputPrintingType: 'PDF_10x15_300dpi' }, + letter: { + service: { + productCode: 'DOM', + depositDate: new Date().toISOString().split('T')[0] + }, + parcel: { weight: 0.5 }, + sender: { address: SENDER_ADDRESS }, + addressee: { + address: { + lastName: 'Test', + line2: '1 Rue de Rivoli', + countryCode: 'FR', + city: 'Paris', + zipCode: '75001' + } + } + } + } + + const response = await axios.post( + `${COLISSIMO_BASE_URL}/checkGenerateLabel`, + testPayload, + { + headers: { 'Content-Type': 'application/json', Accept: '*/*' }, + responseType: 'arraybuffer', + timeout: 15000, + validateStatus: () => true + } + ) + + const responseContentType = response.headers['content-type'] ?? '' + const responseBuffer = Buffer.from(response.data) + + // Si erreur HTTP, tenter de lire le body brut + if (response.status >= 400) { + let bodyText = responseBuffer.toString('utf-8').substring(0, 2000) + let parsed: any = null + try { parsed = JSON.parse(bodyText) } catch { /* ignore */ } + + return res.status(response.status).json({ + connected: false, + httpStatus: response.status, + detail: parsed ?? bodyText + }) + } + + const { json: labelJson } = parseColissimoMtomResponse( + responseBuffer, + responseContentType + ) + + const messages = labelJson?.messages ?? [] + const hasError = messages.some((m: any) => m.type === 'ERROR') + + if (hasError) { + return res.status(400).json({ connected: false, messages }) + } + + return res.json({ connected: true, messages }) + } catch (err) { + const error = err as any + const message = error?.message ? String(error.message) : 'unknown_error' + console.error('GET /admin/colissimo/test failed', { message }) + + if (message.includes('colissimo_credentials_missing')) { + return res.status(500).json({ connected: false, message }) + } + + return res.status(502).json({ connected: false, message: 'colissimo_error', detail: message }) + } +}) + +// --- SSE (Server-Sent Events) --- + +const sseClients = new Set() + +app.get('/admin/events', (req, res) => { + res.setHeader('Content-Type', 'text/event-stream') + res.setHeader('Cache-Control', 'no-cache') + res.setHeader('Connection', 'keep-alive') + res.setHeader('X-Accel-Buffering', 'no') + res.flushHeaders() + + res.write('data: {"type":"connected"}\n\n') + + sseClients.add(res) + + req.on('close', () => { + sseClients.delete(res) + }) +}) + +const broadcastSSE = (eventType: string, data: Record) => { + const payload = JSON.stringify({ type: eventType, ...data }) + for (const client of sseClients) { + try { + client.write(`data: ${payload}\n\n`) + } catch { + sseClients.delete(client) + } + } +} + +// --- Amazon Polling --- + +const POLL_INTERVAL_MS = Number(process.env.AMAZON_POLL_INTERVAL_MS) || 5 * 60 * 1000 +let lastPollAt: Date | null = null +let pollRunning = false + +const pollAmazonOrders = async () => { + if (pollRunning) { + console.log('[amazon-poll] Skipping, previous poll still running') + return + } + + pollRunning = true + const startedAt = new Date() + let ordersFound = 0 + let ordersImported = 0 + let pollError: string | null = null + + try { + const db = getDb() + + let sp: any + try { + sp = await createAmazonClient() + } catch { + // Pas de credentials configurées — skip silencieux + pollRunning = false + return + } + + const createdAfter = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString() + + const response: any = await sp.callAPI({ + operation: 'getOrders', + endpoint: 'orders', + query: { + MarketplaceIds: [AMAZON_MARKETPLACE_FR], + CreatedAfter: createdAfter, + OrderStatuses: ['Unshipped', 'PartiallyShipped'] + } + }) + + const amazonOrders = response?.Orders ?? response?.payload?.Orders ?? [] + ordersFound = amazonOrders.length + + for (const amazonOrder of amazonOrders) { + const amazonOrderId = amazonOrder.AmazonOrderId + if (!amazonOrderId) continue + + const [existing] = await db + .select({ id: orders.id }) + .from(orders) + .where(eq(orders.orderRef, amazonOrderId)) + + if (existing) continue + + try { + const itemsResponse = await sp.callAPI({ + operation: 'getOrderItems', + endpoint: 'orders', + path: { orderId: amazonOrderId } + }) + + const amazonItems = itemsResponse?.OrderItems ?? itemsResponse?.payload?.OrderItems ?? [] + if (amazonItems.length === 0) continue + + const [newOrder] = await db + .insert(orders) + .values({ orderRef: amazonOrderId, status: 'new' }) + .returning() + + await db.insert(orderItems).values( + amazonItems.map((item: any) => ({ + orderId: newOrder.id, + amazonSku: item.SellerSKU ?? 'UNKNOWN', + amazonOrderItemId: item.OrderItemId ?? null, + quantity: item.QuantityOrdered ?? 1, + title: item.Title ?? null + })) + ) + + ordersImported++ + console.log(`[amazon-poll] Imported order ${amazonOrderId} -> ${newOrder.id}`) + + broadcastSSE('amazon-order-imported', { + orderId: newOrder.id, + orderRef: amazonOrderId, + itemCount: amazonItems.length + }) + + // Récupérer l'adresse de livraison + try { + await fetchAndStoreShippingAddress(sp, newOrder.id, amazonOrderId) + } catch (addrErr) { + console.error(`[amazon-poll] Address fetch failed for ${amazonOrderId}:`, addrErr) + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + // Doublon concurrent (import manuel + poll simultané) + if ((err as any)?.code === '23505') continue + console.error(`[amazon-poll] Failed to import ${amazonOrderId}:`, msg) + } + } + + lastPollAt = new Date() + + if (ordersImported > 0) { + broadcastSSE('amazon-poll-complete', { ordersFound, ordersImported }) + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + pollError = msg + console.error('[amazon-poll] Poll failed:', msg) + } finally { + try { + const db = getDb() + await db.insert(amazonPollLog).values({ + startedAt, + finishedAt: new Date(), + ordersFound, + ordersImported, + error: pollError + }) + } catch (logErr) { + console.error('[amazon-poll] Failed to write poll log:', logErr) + } + + pollRunning = false + } +} + +// Statut du polling Amazon +app.get('/admin/amazon/poll-status', async (_req, res) => { + try { + const db = getDb() + const [lastLog] = await db + .select() + .from(amazonPollLog) + .orderBy(desc(amazonPollLog.startedAt)) + .limit(1) + + return res.json({ + enabled: true, + intervalMs: POLL_INTERVAL_MS, + pollRunning, + lastPollAt, + lastLog: lastLog ?? null + }) + } catch (err) { + console.error('GET /admin/amazon/poll-status failed', err) + return res.status(500).json({ message: 'internal_error' }) + } +}) + +// Forcer un poll immédiat +app.post('/admin/amazon/poll-now', async (_req, res) => { + if (pollRunning) { + return res.status(409).json({ message: 'poll_already_running' }) + } + + void pollAmazonOrders() + + return res.json({ message: 'poll_triggered' }) +}) + +// --- Start --- + +const port = Number(process.env.PORT ?? 4000) + +const server = app.listen(port, () => { + console.log(`API listening on port ${port}`) + + // Démarrer le polling Amazon après 30s + setTimeout(() => { + console.log(`[amazon-poll] Starting polling, interval: ${POLL_INTERVAL_MS}ms`) + void pollAmazonOrders() + setInterval(() => void pollAmazonOrders(), POLL_INTERVAL_MS) + }, 30_000) +}) + +// Graceful shutdown +const shutdown = async (signal: string) => { + console.log(`\n[shutdown] ${signal} received, closing...`) + server.close() + const pool = getPool() + if (pool) { + await pool.end() + } + process.exit(0) +} + +process.on('SIGTERM', () => void shutdown('SIGTERM')) +process.on('SIGINT', () => void shutdown('SIGINT')) diff --git a/apps/api/src/seed-admin.ts b/apps/api/src/seed-admin.ts new file mode 100644 index 0000000..1004262 --- /dev/null +++ b/apps/api/src/seed-admin.ts @@ -0,0 +1,36 @@ +import bcrypt from 'bcryptjs' +import { eq } from 'drizzle-orm' +import { getDb } from './db/index.js' +import { users } from './db/schema/index.js' + +const [, , username, password, displayName] = process.argv + +if (!username || !password || !displayName) { + console.error('Usage: pnpm -F @localiztoi/api seed:admin ""') + process.exit(1) +} + +const run = async () => { + const db = getDb() + + const [existing] = await db.select({ id: users.id }).from(users).where(eq(users.username, username)) + if (existing) { + console.log(`L'utilisateur "${username}" existe déjà.`) + process.exit(0) + } + + const passwordHash = await bcrypt.hash(password, 12) + + const [created] = await db + .insert(users) + .values({ username, passwordHash, displayName }) + .returning() + + console.log(`Admin créé : ${created.username} (${created.displayName}) — id: ${created.id}`) + process.exit(0) +} + +run().catch((err) => { + console.error('Erreur:', err) + process.exit(1) +}) diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 0000000..73bc553 --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "types": ["node"], + "resolveJsonModule": true + }, + "include": ["src"] +} diff --git a/apps/web/.gitignore b/apps/web/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/apps/web/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/apps/web/README.md b/apps/web/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/apps/web/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/apps/web/components.json b/apps/web/components.json new file mode 100644 index 0000000..03909d9 --- /dev/null +++ b/apps/web/components.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/apps/web/eslint.config.mjs b/apps/web/eslint.config.mjs new file mode 100644 index 0000000..05e726d --- /dev/null +++ b/apps/web/eslint.config.mjs @@ -0,0 +1,18 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]); + +export default eslintConfig; diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts new file mode 100644 index 0000000..3c83b9c --- /dev/null +++ b/apps/web/next.config.ts @@ -0,0 +1,22 @@ +import type { NextConfig } from 'next' + +const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:4000' + +const nextConfig: NextConfig = { + reactCompiler: true, + allowedDevOrigins: ['http://localhost:3000', 'http://localhost:3001', 'http://192.168.1.50:3000'], + async rewrites() { + return [ + { + source: '/api/:path*', + destination: `${API_BASE_URL}/:path*`, + }, + { + source: '/labels/:path*', + destination: `${API_BASE_URL}/labels/:path*`, + }, + ] + }, +} + +export default nextConfig diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..1206677 --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,40 @@ +{ + "name": "web", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint" + }, + "dependencies": { + "@localiztoi/shared": "workspace:*", + "@tailwindcss/postcss": "^4.1.18", + "axios": "^1.13.5", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "html5-qrcode": "^2.3.8", + "lucide-react": "^0.574.0", + "next": "16.1.6", + "next-themes": "^0.4.6", + "postcss": "^8.5.6", + "radix-ui": "^1.4.3", + "react": "19.2.3", + "react-dom": "19.2.3", + "sonner": "^2.0.7", + "tailwind-merge": "^3.4.1", + "tailwindcss": "^4.1.18" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "babel-plugin-react-compiler": "1.0.0", + "eslint": "^9", + "eslint-config-next": "16.1.6", + "shadcn": "^3.8.5", + "tw-animate-css": "^1.4.0", + "typescript": "^5" + } +} diff --git a/apps/web/postcss.config.mjs b/apps/web/postcss.config.mjs new file mode 100644 index 0000000..2f8795a --- /dev/null +++ b/apps/web/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +} + +export default config diff --git a/apps/web/public/file.svg b/apps/web/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/apps/web/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/public/globe.svg b/apps/web/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/apps/web/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/public/next.svg b/apps/web/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/apps/web/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/public/vercel.svg b/apps/web/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/apps/web/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/public/window.svg b/apps/web/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/apps/web/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/app/admin/amazon-orders/page.tsx b/apps/web/src/app/admin/amazon-orders/page.tsx new file mode 100644 index 0000000..c5be0e4 --- /dev/null +++ b/apps/web/src/app/admin/amazon-orders/page.tsx @@ -0,0 +1,5 @@ +import { AmazonOrdersPage } from '@/components/admin/AmazonOrdersPage' + +export default function Page() { + return +} diff --git a/apps/web/src/app/admin/api-keys/page.tsx b/apps/web/src/app/admin/api-keys/page.tsx new file mode 100644 index 0000000..997aee1 --- /dev/null +++ b/apps/web/src/app/admin/api-keys/page.tsx @@ -0,0 +1,5 @@ +import { ApiKeysPage } from '@/components/admin/ApiKeysPage' + +export default function Page() { + return +} diff --git a/apps/web/src/app/admin/axonaut-stock/page.tsx b/apps/web/src/app/admin/axonaut-stock/page.tsx new file mode 100644 index 0000000..5544d59 --- /dev/null +++ b/apps/web/src/app/admin/axonaut-stock/page.tsx @@ -0,0 +1,5 @@ +import { AxonautStockPage } from '@/components/admin/AxonautStockPage' + +export default function Page() { + return +} diff --git a/apps/web/src/app/admin/layout.tsx b/apps/web/src/app/admin/layout.tsx new file mode 100644 index 0000000..0632ced --- /dev/null +++ b/apps/web/src/app/admin/layout.tsx @@ -0,0 +1,32 @@ +'use client' + +import type { ReactNode } from 'react' + +import { useAuth } from '@/hooks/useAuth' +import { AdminShell } from '@/components/admin/AdminShell' +import { Skeleton } from '@/components/ui/skeleton' + +export default function AdminLayout({ children }: { children: ReactNode }) { + const { user, loading, logout } = useAuth() + + if (loading) { + return ( +
+
+ + +
+
+ ) + } + + if (!user) { + return null + } + + return ( + + {children} + + ) +} diff --git a/apps/web/src/app/admin/orders/[id]/page.tsx b/apps/web/src/app/admin/orders/[id]/page.tsx new file mode 100644 index 0000000..f20faec --- /dev/null +++ b/apps/web/src/app/admin/orders/[id]/page.tsx @@ -0,0 +1,10 @@ +import { OrderDetailsPage } from '@/components/admin/orders/OrderDetailsPage' + +type Props = { + params: Promise<{ id: string }> +} + +export default async function Page(props: Props) { + const { id } = await props.params + return +} diff --git a/apps/web/src/app/admin/orders/[id]/scan-tracking/page.tsx b/apps/web/src/app/admin/orders/[id]/scan-tracking/page.tsx new file mode 100644 index 0000000..dada529 --- /dev/null +++ b/apps/web/src/app/admin/orders/[id]/scan-tracking/page.tsx @@ -0,0 +1,10 @@ +import { ScanTrackingPage } from '@/components/admin/orders/ScanTrackingPage' + +type Props = { + params: Promise<{ id: string }> +} + +export default async function Page(props: Props) { + const { id } = await props.params + return +} diff --git a/apps/web/src/app/admin/orders/[id]/scan/page.tsx b/apps/web/src/app/admin/orders/[id]/scan/page.tsx new file mode 100644 index 0000000..0265c7a --- /dev/null +++ b/apps/web/src/app/admin/orders/[id]/scan/page.tsx @@ -0,0 +1,10 @@ +import { ScanPage } from '@/components/admin/orders/ScanPage' + +type Props = { + params: Promise<{ id: string }> +} + +export default async function Page(props: Props) { + const { id } = await props.params + return +} diff --git a/apps/web/src/app/admin/orders/new/page.tsx b/apps/web/src/app/admin/orders/new/page.tsx new file mode 100644 index 0000000..2fc4707 --- /dev/null +++ b/apps/web/src/app/admin/orders/new/page.tsx @@ -0,0 +1,5 @@ +import { NewOrderPage } from '@/components/admin/orders/NewOrderPage' + +export default function Page() { + return +} diff --git a/apps/web/src/app/admin/orders/page.tsx b/apps/web/src/app/admin/orders/page.tsx new file mode 100644 index 0000000..5363cb5 --- /dev/null +++ b/apps/web/src/app/admin/orders/page.tsx @@ -0,0 +1,5 @@ +import { OrdersListPage } from '@/components/admin/orders/OrdersListPage' + +export default function Page() { + return +} diff --git a/apps/web/src/app/admin/page.tsx b/apps/web/src/app/admin/page.tsx new file mode 100644 index 0000000..6f90460 --- /dev/null +++ b/apps/web/src/app/admin/page.tsx @@ -0,0 +1,5 @@ +import { AdminDashboardPage } from '@/components/admin/AdminDashboardPage' + +export default function AdminPage() { + return +} diff --git a/apps/web/src/app/admin/sku-mappings/page.tsx b/apps/web/src/app/admin/sku-mappings/page.tsx new file mode 100644 index 0000000..6507cf6 --- /dev/null +++ b/apps/web/src/app/admin/sku-mappings/page.tsx @@ -0,0 +1,5 @@ +import { SkuMappingsPage } from '@/components/admin/SkuMappingsPage' + +export default function Page() { + return +} diff --git a/apps/web/src/app/admin/users/page.tsx b/apps/web/src/app/admin/users/page.tsx new file mode 100644 index 0000000..b7dfade --- /dev/null +++ b/apps/web/src/app/admin/users/page.tsx @@ -0,0 +1,5 @@ +import { UsersPage } from '@/components/admin/UsersPage' + +export default function Page() { + return +} diff --git a/apps/web/src/app/favicon.ico b/apps/web/src/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/apps/web/src/app/favicon.ico differ diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css new file mode 100644 index 0000000..bd85122 --- /dev/null +++ b/apps/web/src/app/globals.css @@ -0,0 +1,130 @@ +@import "tailwindcss"; +@import "tw-animate-css"; +@import "shadcn/tailwind.css"; + +@custom-variant dark (&:is(.dark *)); + +html, +body { + max-width: 100vw; + overflow-x: hidden; +} + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} \ No newline at end of file diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx new file mode 100644 index 0000000..708f41d --- /dev/null +++ b/apps/web/src/app/layout.tsx @@ -0,0 +1,47 @@ +import type { Metadata, Viewport } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import { Toaster } from "@/components/ui/sonner"; +import { cn } from "@/lib/utils"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Gestion Stock - Localiztoi", + description: "Gestion de stock et préparation de commandes", +}; + +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + maximumScale: 1, +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + + ); +} diff --git a/apps/web/src/app/login/page.tsx b/apps/web/src/app/login/page.tsx new file mode 100644 index 0000000..0fdfc48 --- /dev/null +++ b/apps/web/src/app/login/page.tsx @@ -0,0 +1,5 @@ +import { LoginPage } from '@/components/auth/LoginPage' + +export default function Page() { + return +} diff --git a/apps/web/src/app/page.module.css b/apps/web/src/app/page.module.css new file mode 100644 index 0000000..59dea42 --- /dev/null +++ b/apps/web/src/app/page.module.css @@ -0,0 +1,141 @@ +.page { + --background: #fafafa; + --foreground: #fff; + + --text-primary: #000; + --text-secondary: #666; + + --button-primary-hover: #383838; + --button-secondary-hover: #f2f2f2; + --button-secondary-border: #ebebeb; + + display: flex; + min-height: 100vh; + align-items: center; + justify-content: center; + font-family: var(--font-geist-sans); + background-color: var(--background); +} + +.main { + display: flex; + min-height: 100vh; + width: 100%; + max-width: 800px; + flex-direction: column; + align-items: flex-start; + justify-content: space-between; + background-color: var(--foreground); + padding: 120px 60px; +} + +.intro { + display: flex; + flex-direction: column; + align-items: flex-start; + text-align: left; + gap: 24px; +} + +.intro h1 { + max-width: 320px; + font-size: 40px; + font-weight: 600; + line-height: 48px; + letter-spacing: -2.4px; + text-wrap: balance; + color: var(--text-primary); +} + +.intro p { + max-width: 440px; + font-size: 18px; + line-height: 32px; + text-wrap: balance; + color: var(--text-secondary); +} + +.intro a { + font-weight: 500; + color: var(--text-primary); +} + +.ctas { + display: flex; + flex-direction: row; + width: 100%; + max-width: 440px; + gap: 16px; + font-size: 14px; +} + +.ctas a { + display: flex; + justify-content: center; + align-items: center; + height: 40px; + padding: 0 16px; + border-radius: 128px; + border: 1px solid transparent; + transition: 0.2s; + cursor: pointer; + width: fit-content; + font-weight: 500; +} + +a.primary { + background: var(--text-primary); + color: var(--background); + gap: 8px; +} + +a.secondary { + border-color: var(--button-secondary-border); +} + +/* Enable hover only on non-touch devices */ +@media (hover: hover) and (pointer: fine) { + a.primary:hover { + background: var(--button-primary-hover); + border-color: transparent; + } + + a.secondary:hover { + background: var(--button-secondary-hover); + border-color: transparent; + } +} + +@media (max-width: 600px) { + .main { + padding: 48px 24px; + } + + .intro { + gap: 16px; + } + + .intro h1 { + font-size: 32px; + line-height: 40px; + letter-spacing: -1.92px; + } +} + +@media (prefers-color-scheme: dark) { + .logo { + filter: invert(); + } + + .page { + --background: #000; + --foreground: #000; + + --text-primary: #ededed; + --text-secondary: #999; + + --button-primary-hover: #ccc; + --button-secondary-hover: #1a1a1a; + --button-secondary-border: #1a1a1a; + } +} diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx new file mode 100644 index 0000000..80986aa --- /dev/null +++ b/apps/web/src/app/page.tsx @@ -0,0 +1,20 @@ +import Link from 'next/link' +import { Package } from 'lucide-react' +import { Button } from '@/components/ui/button' + +export default function Home() { + return ( +
+
+ +

Localiztoi Stock

+
+

+ Gestion de stock et préparation de commandes +

+ +
+ ) +} diff --git a/apps/web/src/components/admin/AdminDashboardPage.tsx b/apps/web/src/components/admin/AdminDashboardPage.tsx new file mode 100644 index 0000000..777e65d --- /dev/null +++ b/apps/web/src/components/admin/AdminDashboardPage.tsx @@ -0,0 +1,249 @@ +'use client' + +import Link from 'next/link' +import { useEffect, useMemo, useState } from 'react' +import { RefreshCw, Plus, Package, ArrowLeftRight, ScanLine, Radio } from 'lucide-react' + +import { api } from '@/config/api' +import { ensureErrorMessage } from '@/helpers/ensureErrorMessage' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Badge } from '@/components/ui/badge' + +type OrderRow = { + id: string + status: string + createdAt: string +} + +type PollStatus = { + enabled: boolean + intervalMs: number + pollRunning: boolean + lastPollAt: string | null + lastLog: { + startedAt: string + finishedAt: string | null + ordersFound: number + ordersImported: number + error: string | null + } | null +} + +export const AdminDashboardPage = () => { + const [orders, setOrders] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [pollStatus, setPollStatus] = useState(null) + const [pollTriggering, setPollTriggering] = useState(false) + + const load = async () => { + setLoading(true) + setError(null) + + try { + const res = await api.get('/admin/orders') + setOrders(res.data) + } catch (err) { + setError(ensureErrorMessage(err)) + } finally { + setLoading(false) + } + } + + const loadPollStatus = async () => { + try { + const res = await api.get('/admin/amazon/poll-status') + setPollStatus(res.data) + } catch { + // ignore + } + } + + const triggerPoll = async () => { + setPollTriggering(true) + try { + await api.post('/admin/amazon/poll-now') + // Rafraîchir le statut après 3s (le temps que le poll s'exécute) + setTimeout(() => void loadPollStatus(), 3000) + } catch { + // ignore + } finally { + setPollTriggering(false) + } + } + + useEffect(() => { + void load() + void loadPollStatus() + + const interval = setInterval(() => void loadPollStatus(), 30_000) + return () => clearInterval(interval) + }, []) + + const orderCount = orders.length + const newCount = useMemo(() => orders.filter((o) => o.status === 'new').length, [orders]) + + const lastCreatedAt = useMemo(() => { + const last = orders[0]?.createdAt + if (!last) { + return null + } + return new Date(last).toLocaleString() + }, [orders]) + + return ( +
+
+
+

Dashboard

+

Vue rapide sur l'activité

+
+ +
+ + {error ? ( + + {error} + + ) : null} + +
+ + + Commandes + + +
{orderCount}
+ {lastCreatedAt ? ( +

Dernière: {lastCreatedAt}

+ ) : null} +
+
+ + + + À préparer + + +
{newCount}
+

Commandes en attente

+
+
+ + + + Scanner + + +
+ + Prêt +
+

Scan QR/barcode dans le détail commande

+
+
+
+ +
+ + + Raccourcis + + + + + + + + + + +
+ + + Polling Amazon + + {pollStatus ? ( + + {pollStatus.pollRunning ? 'En cours…' : 'Actif'} + + ) : null} +
+
+ + {pollStatus ? ( + <> +
+
+ Intervalle + {Math.round(pollStatus.intervalMs / 60000)} min +
+ {pollStatus.lastPollAt ? ( +
+ Dernier poll + {new Date(pollStatus.lastPollAt).toLocaleTimeString()} +
+ ) : null} + {pollStatus.lastLog ? ( + <> +
+ Trouvées + {pollStatus.lastLog.ordersFound} +
+
+ Importées + {pollStatus.lastLog.ordersImported} +
+ {pollStatus.lastLog.error ? ( +

+ {pollStatus.lastLog.error} +

+ ) : null} + + ) : ( +

Pas encore de résultat

+ )} +
+ + + ) : ( +

Chargement…

+ )} +
+
+
+
+ ) +} diff --git a/apps/web/src/components/admin/AdminShell.tsx b/apps/web/src/components/admin/AdminShell.tsx new file mode 100644 index 0000000..99947ce --- /dev/null +++ b/apps/web/src/components/admin/AdminShell.tsx @@ -0,0 +1,162 @@ +'use client' + +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import { useState, type ReactNode } from 'react' +import { Menu, LayoutDashboard, Package, ArrowLeftRight, Plus, KeyRound, PackageSearch, ShoppingCart, LogOut, Users } from 'lucide-react' +import type { AuthUser } from '@localiztoi/shared' + +import { Button } from '@/components/ui/button' +import { + Sheet, + SheetContent, + SheetTrigger, + SheetTitle, +} from '@/components/ui/sheet' +import { cn } from '@/lib/utils' + +const isActivePath = (pathname: string, href: string) => { + if (href === '/admin') { + return pathname === '/admin' + } + return pathname === href || pathname.startsWith(`${href}/`) +} + +const navLinks = [ + { href: '/admin', label: 'Dashboard', icon: LayoutDashboard }, + { href: '/admin/amazon-orders', label: 'Amazon', icon: ShoppingCart }, + { href: '/admin/orders', label: 'Commandes', icon: Package }, + { href: '/admin/sku-mappings', label: 'Correspondances SKU', icon: ArrowLeftRight }, + { href: '/admin/axonaut-stock', label: 'Stock Axonaut', icon: PackageSearch }, + { href: '/admin/api-keys', label: 'Tokens API', icon: KeyRound }, + { href: '/admin/users', label: 'Utilisateurs', icon: Users }, +] + +const NavContent = ({ + pathname, + onNavigate, +}: { + pathname: string + onNavigate?: () => void +}) => ( + +) + +export const AdminShell = ({ children, user, onLogout }: { children: ReactNode; user: AuthUser; onLogout: () => void }) => { + const pathname = usePathname() + const [mobileMenuOpen, setMobileMenuOpen] = useState(false) + + // Pages en mode plein écran (sans header ni sidebar) + const isFullscreen = pathname.match(/\/admin\/orders\/[^/]+\/scan(-tracking)?$/) + + if (isFullscreen) { + return ( +
+
+ {children} +
+
+ ) + } + + return ( +
+ {/* Desktop sidebar */} + + + {/* Main area */} +
+ {/* Header */} +
+
+ {/* Mobile hamburger */} + + + + + + + Localiztoi Stock + +
+ setMobileMenuOpen(false)} + /> +
+
+
+
+

{user.displayName}

+

{user.username}

+
+ +
+
+
+
+ + Localiztoi + {pathname} +
+ + +
+ + {/* Page content */} +
+ {children} +
+
+
+ ) +} diff --git a/apps/web/src/components/admin/AmazonOrdersPage.tsx b/apps/web/src/components/admin/AmazonOrdersPage.tsx new file mode 100644 index 0000000..b3f68de --- /dev/null +++ b/apps/web/src/components/admin/AmazonOrdersPage.tsx @@ -0,0 +1,343 @@ +'use client' + +import { useEffect, useRef, useState } from 'react' +import { useRouter } from 'next/navigation' +import { RefreshCw, Download, Check, Radio } from 'lucide-react' +import { toast } from 'sonner' + +import { api } from '@/config/api' +import { ensureErrorMessage } from '@/helpers/ensureErrorMessage' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Badge } from '@/components/ui/badge' +import { Skeleton } from '@/components/ui/skeleton' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' + +type AmazonOrder = { + AmazonOrderId: string + PurchaseDate: string + OrderStatus: string + OrderTotal?: { CurrencyCode: string; Amount: string } + NumberOfItemsUnshipped: number + NumberOfItemsShipped: number + FulfillmentChannel: string + SalesChannel?: string + EarliestShipDate?: string + LatestShipDate?: string +} + +const statusLabels: Record = { + Unshipped: { label: 'Non expédiée', variant: 'default' }, + PartiallyShipped: { label: 'Partielle', variant: 'secondary' }, + Shipped: { label: 'Expédiée', variant: 'outline' }, + Canceled: { label: 'Annulée', variant: 'destructive' }, + Pending: { label: 'En attente', variant: 'secondary' }, +} + +export const AmazonOrdersPage = () => { + const router = useRouter() + const [amazonOrders, setAmazonOrders] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [loaded, setLoaded] = useState(false) + const [importing, setImporting] = useState(null) + const [importedIds, setImportedIds] = useState>(new Set()) + const [importError, setImportError] = useState(null) + + const [sseConnected, setSseConnected] = useState(false) + const loadRef = useRef<() => Promise>(undefined) + + const load = async () => { + setLoading(true) + setError(null) + + try { + const res = await api.get<{ orders: AmazonOrder[]; count: number }>('/admin/amazon/orders') + setAmazonOrders(res.data.orders ?? []) + setLoaded(true) + } catch (err) { + setError(ensureErrorMessage(err)) + } finally { + setLoading(false) + } + } + + loadRef.current = load + + // Auto-load au mount + SSE + fallback polling + useEffect(() => { + void load() + + const eventSource = new EventSource('/api/admin/events') + + eventSource.onopen = () => { + setSseConnected(true) + } + + eventSource.onmessage = (event) => { + try { + const data = JSON.parse(event.data) + if (data.type === 'amazon-order-imported') { + toast.info(`Nouvelle commande Amazon : ${data.orderRef}`) + void loadRef.current?.() + } else if (data.type === 'amazon-poll-complete') { + void loadRef.current?.() + } + } catch { + // ignore + } + } + + eventSource.onerror = () => { + setSseConnected(false) + } + + // Fallback polling 60s si SSE échoue + const fallbackInterval = setInterval(() => { + if (eventSource.readyState !== EventSource.OPEN) { + void loadRef.current?.() + } + }, 60_000) + + return () => { + eventSource.close() + clearInterval(fallbackInterval) + } + }, []) + + const importOrder = async (amazonOrderId: string) => { + setImporting(amazonOrderId) + setImportError(null) + + try { + const res = await api.post<{ orderId: string }>('/admin/amazon/import', { amazonOrderId }) + setImportedIds((prev) => new Set(prev).add(amazonOrderId)) + + // Rediriger vers la commande importée après 1s + const orderId = res.data.orderId + setTimeout(() => { + router.push(`/admin/orders/${orderId}`) + }, 1000) + } catch (err) { + const msg = ensureErrorMessage(err) + if (msg.includes('order_already_exists')) { + setImportedIds((prev) => new Set(prev).add(amazonOrderId)) + setImportError(`La commande ${amazonOrderId} existe déjà.`) + } else { + setImportError(msg) + } + } finally { + setImporting(null) + } + } + + const formatDate = (iso: string) => { + try { + return new Date(iso).toLocaleDateString('fr-FR', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) + } catch { + return iso + } + } + + const formatShipDeadline = (order: AmazonOrder) => { + if (!order.LatestShipDate) return null + try { + const deadline = new Date(order.LatestShipDate) + const now = new Date() + const diffMs = deadline.getTime() - now.getTime() + const diffHours = Math.floor(diffMs / (1000 * 60 * 60)) + + if (diffHours < 0) return 'Dépassée' + if (diffHours < 24) return `${diffHours}h` + return `${Math.floor(diffHours / 24)}j` + } catch { + return null + } + } + + return ( +
+
+
+

Commandes Amazon

+

+ Commandes à expédier depuis Amazon Seller Central + + + {sseConnected ? 'Temps réel' : 'Hors ligne'} + +

+
+ +
+ + {error ? ( + + {error} + + ) : null} + + {importError ? ( + + {importError} + + ) : null} + + {loading && !loaded ? ( +
+ + + +
+ ) : null} + + {loaded ? ( + + + + Commandes ({amazonOrders.length}) + + + + {amazonOrders.length === 0 ? ( +

+ Aucune commande non expédiée trouvée sur les 7 derniers jours. +

+ ) : ( + <> + {/* Mobile: cartes */} +
+ {amazonOrders.map((order) => { + const imported = importedIds.has(order.AmazonOrderId) + const isImporting = importing === order.AmazonOrderId + const status = statusLabels[order.OrderStatus] + const shipDeadline = formatShipDeadline(order) + + return ( +
+
+
+

{order.AmazonOrderId}

+

{formatDate(order.PurchaseDate)}

+
+ + {status?.label ?? order.OrderStatus} + +
+ +
+ {order.NumberOfItemsUnshipped} article{order.NumberOfItemsUnshipped > 1 ? 's' : ''} + {order.OrderTotal ? ( + {order.OrderTotal.Amount} {order.OrderTotal.CurrencyCode} + ) : null} + {shipDeadline ? Expédier : {shipDeadline} : null} +
+ + +
+ ) + })} +
+ + {/* Desktop: table */} +
+ + + + Commande + Date + Statut + Articles + Total + Expédier avant + Action + + + + {amazonOrders.map((order) => { + const imported = importedIds.has(order.AmazonOrderId) + const isImporting = importing === order.AmazonOrderId + const status = statusLabels[order.OrderStatus] + const shipDeadline = formatShipDeadline(order) + + return ( + + {order.AmazonOrderId} + {formatDate(order.PurchaseDate)} + + + {status?.label ?? order.OrderStatus} + + + {order.NumberOfItemsUnshipped} + + {order.OrderTotal ? `${order.OrderTotal.Amount} ${order.OrderTotal.CurrencyCode}` : '—'} + + + {shipDeadline ? ( + + {shipDeadline} + + ) : '—'} + + + + + + ) + })} + +
+
+ + )} +
+
+ ) : null} +
+ ) +} diff --git a/apps/web/src/components/admin/ApiKeysPage.tsx b/apps/web/src/components/admin/ApiKeysPage.tsx new file mode 100644 index 0000000..5fd441b --- /dev/null +++ b/apps/web/src/components/admin/ApiKeysPage.tsx @@ -0,0 +1,379 @@ +'use client' + +import { useEffect, useMemo, useState } from 'react' +import { RefreshCw, Plus, Trash2, Pencil, Key } from 'lucide-react' +import type { ApiProvider, ApiKeyCreate } from '@localiztoi/shared' +import { apiProviders, apiProviderLabels } from '@localiztoi/shared' + +import { api } from '@/config/api' +import { ensureErrorMessage } from '@/helpers/ensureErrorMessage' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Skeleton } from '@/components/ui/skeleton' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog' + +type ApiKeyRecord = { + id: string + provider: ApiProvider + label: string + value: string + createdAt: string + updatedAt: string +} + +const suggestedLabels: Partial> = { + amazon: ['client_id', 'client_secret', 'refresh_token'], + axonaut: ['api_key'], + fota_teltonika: ['api_token', 'user_agent'], + '1nce': ['api_token'], + laposte: ['contract_number', 'password'], +} + +export const ApiKeysPage = () => { + const [items, setItems] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + // Form state + const [formProvider, setFormProvider] = useState('amazon') + const [formLabel, setFormLabel] = useState('') + const [formCustomLabel, setFormCustomLabel] = useState('') + const [formValue, setFormValue] = useState('') + + // Edit state + const [editingId, setEditingId] = useState(null) + const [editValue, setEditValue] = useState('') + + const effectiveLabel = formLabel === '__custom' ? formCustomLabel : formLabel + + const canSubmit = useMemo(() => { + const label = formLabel === '__custom' ? formCustomLabel : formLabel + return label.trim().length > 0 && formValue.trim().length > 0 + }, [formLabel, formCustomLabel, formValue]) + + const load = async () => { + setLoading(true) + setError(null) + + try { + const res = await api.get('/admin/api-keys') + setItems(res.data) + } catch (err) { + setError(ensureErrorMessage(err)) + } finally { + setLoading(false) + } + } + + useEffect(() => { + void load() + }, []) + + const onCreate = async () => { + if (!canSubmit) return + + setLoading(true) + setError(null) + + try { + const payload: ApiKeyCreate = { + provider: formProvider, + label: effectiveLabel.trim(), + value: formValue.trim() + } + await api.post('/admin/api-keys', payload) + setFormLabel('') + setFormCustomLabel('') + setFormValue('') + await load() + } catch (err) { + setError(ensureErrorMessage(err)) + } finally { + setLoading(false) + } + } + + const onUpdate = async (id: string) => { + if (!editValue.trim()) return + + setLoading(true) + setError(null) + + try { + await api.patch(`/admin/api-keys/${id}`, { value: editValue.trim() }) + setEditingId(null) + setEditValue('') + await load() + } catch (err) { + setError(ensureErrorMessage(err)) + } finally { + setLoading(false) + } + } + + const onDelete = async (id: string) => { + setLoading(true) + setError(null) + + try { + await api.delete(`/admin/api-keys/${id}`) + await load() + } catch (err) { + setError(ensureErrorMessage(err)) + } finally { + setLoading(false) + } + } + + // Grouper par provider + const grouped = useMemo(() => { + const map = new Map() + for (const p of apiProviders) { + map.set(p, []) + } + for (const item of items) { + const list = map.get(item.provider) + if (list) list.push(item) + } + return map + }, [items]) + + // Labels suggérés non encore ajoutés pour le provider sélectionné + const availableSuggestions = useMemo(() => { + const suggestions = suggestedLabels[formProvider] ?? [] + const existingLabels = (grouped.get(formProvider) ?? []).map((k) => k.label) + return suggestions.filter((s) => !existingLabels.includes(s)) + }, [formProvider, grouped]) + + return ( +
+
+
+

Tokens API

+

+ Clés et tokens pour les services externes +

+
+ +
+ + {error ? ( + + {error} + + ) : null} + + {/* Formulaire d'ajout */} + + + Ajouter un token + + +
+
+ + +
+ +
+ + {availableSuggestions.length > 0 ? ( + + ) : ( + setFormLabel(e.target.value)} + placeholder="ex: api_key" + className="h-11" + /> + )} + {formLabel === '__custom' ? ( + setFormCustomLabel(e.target.value)} + placeholder="Saisir le nom de la clé" + className="h-11" + autoFocus + /> + ) : null} +
+
+ +
+ + setFormValue(e.target.value)} + placeholder="Coller le token ici" + className="h-11" + /> +
+ + +
+
+ + {/* Liste par provider */} + {loading && items.length === 0 ? ( +
+ + +
+ ) : ( + apiProviders.map((provider) => { + const keys = grouped.get(provider) ?? [] + return ( + + + + + {apiProviderLabels[provider]} + {keys.length > 0 ? ( + {keys.length} + ) : null} + + + + {keys.length === 0 ? ( +

Aucun token configuré

+ ) : ( +
+ {keys.map((item) => ( +
+
+

{item.label}

+ {editingId === item.id ? ( +
+ setEditValue(e.target.value)} + placeholder="Nouvelle valeur" + className="h-9 text-sm" + autoFocus + /> + + +
+ ) : ( +

+ {item.value} +

+ )} +
+ + {editingId !== item.id ? ( +
+ + + + + + + + + Supprimer ce token ? + + Le token {item.label} de {apiProviderLabels[provider]} sera définitivement supprimé. + + + + Annuler + void onDelete(item.id)}> + Supprimer + + + + +
+ ) : null} +
+ ))} +
+ )} +
+
+ ) + }) + )} +
+ ) +} diff --git a/apps/web/src/components/admin/AxonautStockPage.tsx b/apps/web/src/components/admin/AxonautStockPage.tsx new file mode 100644 index 0000000..7b90139 --- /dev/null +++ b/apps/web/src/components/admin/AxonautStockPage.tsx @@ -0,0 +1,202 @@ +'use client' + +import { useState } from 'react' +import { RefreshCw, Package } from 'lucide-react' + +import { api } from '@/config/api' +import { ensureErrorMessage } from '@/helpers/ensureErrorMessage' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Badge } from '@/components/ui/badge' +import { Skeleton } from '@/components/ui/skeleton' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' + +type AxonautProduct = { + id: number + internal_id: string | null + product_code: string + name: string + description: string | null + price: number | null + tax_rate: number + type: number + disabled: boolean + stock: string | number | null +} + +const typeLabels: Record = { + 601: 'Matière première', + 701: 'Produit fini', + 706: 'Service', + 707: 'Marchandise', +} + +export const AxonautStockPage = () => { + const [products, setProducts] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [loaded, setLoaded] = useState(false) + + const load = async () => { + setLoading(true) + setError(null) + + try { + const res = await api.get('/admin/axonaut/products') + const data = Array.isArray(res.data) ? res.data : [] + setProducts(data.filter((p) => !p.disabled)) + setLoaded(true) + } catch (err) { + setError(ensureErrorMessage(err)) + } finally { + setLoading(false) + } + } + + const parseStock = (stock: string | number | null | undefined): number | null => { + if (stock === null || stock === undefined) return null + const n = typeof stock === 'number' ? stock : parseFloat(stock) + return Number.isFinite(n) ? Math.round(n) : null + } + + return ( +
+
+
+

Stock Axonaut

+

+ Consultation des niveaux de stock dans Axonaut +

+
+ +
+ + {error ? ( + + {error} + + ) : null} + + {!loaded && !loading ? ( + + + +

+ Cliquez sur Charger pour consulter les stocks Axonaut. +

+
+
+ ) : null} + + {loading && !loaded ? ( +
+ + + + +
+ ) : null} + + {loaded ? ( + + + + Produits ({products.length}) + + + + {/* Mobile: cartes */} +
+ {products.length === 0 ? ( +

Aucun produit trouvé.

+ ) : ( + products.map((p) => { + const stock = parseStock(p.stock) + return ( +
+
+
+

{p.name}

+ {p.product_code ? ( +

{p.product_code}

+ ) : null} +
+ 0 ? 'secondary' : 'outline'} + className={stock !== null && stock <= 0 ? 'text-destructive' : ''} + > + {stock !== null ? stock : '—'} + +
+
+ {typeLabels[p.type] ?? `Type ${p.type}`} + {typeof p.price === 'number' ? p.price.toFixed(2) : '—'} EUR +
+
+ ) + }) + )} +
+ + {/* Desktop: table */} +
+ + + + Produit + Code + Type + Prix HT + Stock + + + + {products.length === 0 ? ( + + + Aucun produit trouvé. + + + ) : ( + products.map((p) => { + const stock = parseStock(p.stock) + return ( + + {p.name} + {p.product_code || '—'} + + {typeLabels[p.type] ?? `Type ${p.type}`} + + {typeof p.price === 'number' ? p.price.toFixed(2) : '—'} EUR + + 0 ? 'secondary' : 'outline'} + className={stock !== null && stock <= 0 ? 'text-destructive' : ''} + > + {stock !== null ? stock : '—'} + + + + ) + }) + )} + +
+
+
+
+ ) : null} +
+ ) +} diff --git a/apps/web/src/components/admin/SkuMappingsPage.tsx b/apps/web/src/components/admin/SkuMappingsPage.tsx new file mode 100644 index 0000000..f776dba --- /dev/null +++ b/apps/web/src/components/admin/SkuMappingsPage.tsx @@ -0,0 +1,324 @@ +'use client' + +import { useEffect, useMemo, useState } from 'react' +import { RefreshCw, Plus, Trash2 } from 'lucide-react' +import type { SkuMapping } from '@localiztoi/shared' + +import { api } from '@/config/api' +import { ensureErrorMessage } from '@/helpers/ensureErrorMessage' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { Skeleton } from '@/components/ui/skeleton' + +type SkuMappingRecord = SkuMapping & { id: string } + +type FormState = SkuMapping + +const emptyForm: FormState = { + amazonSku: '', + enterpriseSku: '', + expectedFotaModel: '', + axonautProductInternalId: '' +} + +export const SkuMappingsPage = () => { + const [items, setItems] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [form, setForm] = useState(emptyForm) + + const canSubmit = useMemo(() => { + return Boolean( + form.amazonSku.trim() && + form.enterpriseSku.trim() && + form.expectedFotaModel.trim() && + form.axonautProductInternalId.trim() + ) + }, [form]) + + const load = async () => { + setLoading(true) + setError(null) + + try { + const res = await api.get('/admin/sku-mappings') + setItems(res.data) + } catch (err) { + setError(ensureErrorMessage(err)) + } finally { + setLoading(false) + } + } + + useEffect(() => { + void load() + }, []) + + const onCreate = async () => { + if (!canSubmit) { + return + } + + setLoading(true) + setError(null) + + try { + await api.post('/admin/sku-mappings', form) + setForm(emptyForm) + await load() + } catch (err) { + setError(ensureErrorMessage(err)) + } finally { + setLoading(false) + } + } + + const onDelete = async (id: string) => { + setLoading(true) + setError(null) + + try { + await api.delete(`/admin/sku-mappings/${id}`) + await load() + } catch (err) { + setError(ensureErrorMessage(err)) + } finally { + setLoading(false) + } + } + + return ( +
+
+
+

Correspondances SKU

+

+ Amazon SKU → SKU entreprise → Modèle FOTA → Produit Axonaut +

+
+ +
+ + {error ? ( + + {error} + + ) : null} + + {/* Formulaire d'ajout */} + + + Ajouter une correspondance + + +
+
+ + setForm({ ...form, amazonSku: e.target.value })} + placeholder="ex: FMC920-EU" + className="h-11" + /> +
+ +
+ + setForm({ ...form, enterpriseSku: e.target.value })} + placeholder="ex: FMC920-EU" + className="h-11" + /> +
+ +
+ + setForm({ ...form, expectedFotaModel: e.target.value })} + placeholder="ex: FMC920" + className="h-11" + /> +
+ +
+ + setForm({ ...form, axonautProductInternalId: e.target.value })} + placeholder="ex: 12345" + className="h-11" + /> +
+
+ + +
+
+ + {/* Liste */} + + + + Liste ({items.length}) + + + + {/* Mobile: cartes */} +
+ {loading && items.length === 0 ? ( + <> + + + + ) : items.length === 0 ? ( +

Aucune correspondance.

+ ) : ( + items.map((it) => ( +
+
+
+

{it.amazonSku}

+

→ {it.enterpriseSku}

+
+ + + + + + + Supprimer cette correspondance ? + + La correspondance pour {it.amazonSku} sera définitivement supprimée. + + + + Annuler + void onDelete(it.id)}> + Supprimer + + + + +
+
+ FOTA: {it.expectedFotaModel} + Réf Axonaut: {it.axonautProductInternalId} +
+
+ )) + )} +
+ + {/* Desktop: table */} +
+ + + + Amazon SKU + SKU entreprise + Modèle FOTA + Réf Axonaut + Actions + + + + {loading && items.length === 0 ? ( + <> + {[1, 2].map((i) => ( + + + + + + + + ))} + + ) : items.length === 0 ? ( + + + Aucune correspondance. + + + ) : ( + items.map((it) => ( + + {it.amazonSku} + {it.enterpriseSku} + {it.expectedFotaModel} + {it.axonautProductInternalId} + + + + + + + + Supprimer cette correspondance ? + + La correspondance pour {it.amazonSku} sera définitivement supprimée. + + + + Annuler + void onDelete(it.id)}> + Supprimer + + + + + + + )) + )} + +
+
+
+
+
+ ) +} diff --git a/apps/web/src/components/admin/UsersPage.tsx b/apps/web/src/components/admin/UsersPage.tsx new file mode 100644 index 0000000..43b7dcb --- /dev/null +++ b/apps/web/src/components/admin/UsersPage.tsx @@ -0,0 +1,333 @@ +'use client' + +import { useEffect, useState } from 'react' +import { RefreshCw, Plus, Trash2, KeyRound } from 'lucide-react' + +import { api } from '@/config/api' +import { useAuth } from '@/hooks/useAuth' +import { ensureErrorMessage } from '@/helpers/ensureErrorMessage' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Skeleton } from '@/components/ui/skeleton' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog' + +type UserRecord = { + id: string + username: string + displayName: string + createdAt: string +} + +export const UsersPage = () => { + const { user: currentUser } = useAuth() + const [users, setUsers] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [success, setSuccess] = useState(null) + + // Form state + const [formUsername, setFormUsername] = useState('') + const [formPassword, setFormPassword] = useState('') + const [formDisplayName, setFormDisplayName] = useState('') + + // Change password state + const [changingPasswordId, setChangingPasswordId] = useState(null) + const [newPassword, setNewPassword] = useState('') + + const canSubmit = formUsername.trim().length >= 3 && formPassword.length >= 8 && formDisplayName.trim().length > 0 + + const load = async () => { + setLoading(true) + setError(null) + + try { + const res = await api.get('/admin/users') + setUsers(res.data) + } catch (err) { + setError(ensureErrorMessage(err)) + } finally { + setLoading(false) + } + } + + useEffect(() => { + void load() + }, []) + + const onCreate = async () => { + if (!canSubmit) return + + setLoading(true) + setError(null) + setSuccess(null) + + try { + await api.post('/admin/users', { + username: formUsername.trim(), + password: formPassword, + displayName: formDisplayName.trim() + }) + setFormUsername('') + setFormPassword('') + setFormDisplayName('') + setSuccess('Utilisateur créé') + await load() + } catch (err) { + const msg = ensureErrorMessage(err) + if (msg.includes('username_taken')) { + setError('Ce nom d\'utilisateur est déjà pris.') + } else { + setError(msg) + } + } finally { + setLoading(false) + } + } + + const onChangePassword = async (id: string) => { + if (newPassword.length < 8) return + + setLoading(true) + setError(null) + setSuccess(null) + + try { + await api.patch(`/admin/users/${id}/password`, { password: newPassword }) + setChangingPasswordId(null) + setNewPassword('') + setSuccess('Mot de passe modifié') + } catch (err) { + setError(ensureErrorMessage(err)) + } finally { + setLoading(false) + } + } + + const onDelete = async (id: string) => { + setLoading(true) + setError(null) + setSuccess(null) + + try { + await api.delete(`/admin/users/${id}`) + setSuccess('Utilisateur supprimé') + await load() + } catch (err) { + const msg = ensureErrorMessage(err) + if (msg.includes('cannot_delete_self')) { + setError('Vous ne pouvez pas supprimer votre propre compte.') + } else { + setError(msg) + } + } finally { + setLoading(false) + } + } + + return ( +
+
+
+

Utilisateurs

+

+ Gérer les comptes utilisateurs +

+
+ +
+ + {error ? ( + + {error} + + ) : null} + + {success ? ( + + {success} + + ) : null} + + {/* Formulaire d'ajout */} + + + Créer un utilisateur + + +
+
+ + setFormUsername(e.target.value)} + placeholder="min. 3 caractères" + autoCapitalize="none" + autoCorrect="off" + className="h-12" + /> +
+ +
+ + setFormDisplayName(e.target.value)} + placeholder="Prénom ou surnom" + className="h-12" + /> +
+ +
+ + setFormPassword(e.target.value)} + placeholder="min. 8 caractères" + className="h-12" + /> +
+
+ + +
+
+ + {/* Liste des utilisateurs */} + {loading && users.length === 0 ? ( +
+ + +
+ ) : ( + + + + Comptes ({users.length}) + + + + {users.length === 0 ? ( +

Aucun utilisateur

+ ) : ( +
+ {users.map((u) => { + const isSelf = currentUser?.id === u.id + + return ( +
+
+

+ {u.displayName} + {isSelf ? ( + (vous) + ) : null} +

+

{u.username}

+ + {changingPasswordId === u.id ? ( +
+ setNewPassword(e.target.value)} + placeholder="Nouveau mot de passe (min. 8)" + className="h-9 text-sm" + autoFocus + /> + + +
+ ) : null} +
+ + {changingPasswordId !== u.id ? ( +
+ + + + + + + + + Supprimer cet utilisateur ? + + Le compte {u.displayName} ({u.username}) sera définitivement supprimé. + + + + Annuler + void onDelete(u.id)}> + Supprimer + + + + +
+ ) : null} +
+ ) + })} +
+ )} +
+
+ )} +
+ ) +} diff --git a/apps/web/src/components/admin/orders/ImeiValidationModal.tsx b/apps/web/src/components/admin/orders/ImeiValidationModal.tsx new file mode 100644 index 0000000..43412ef --- /dev/null +++ b/apps/web/src/components/admin/orders/ImeiValidationModal.tsx @@ -0,0 +1,113 @@ +'use client' + +import { Loader2 } from 'lucide-react' +import { + AlertDialog, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Button } from '@/components/ui/button' +import type { FotaLookupResult } from './types' +import { getFotaActivityStatusLabel } from './types' + +type Props = { + open: boolean + onOpenChange: (open: boolean) => void + lookupResult: FotaLookupResult | null + saving: boolean + saveError: string | null + expectedFotaModels: Set + onConfirm: () => void + onDismiss: () => void +} + +export function ImeiValidationModal({ + open, + onOpenChange, + lookupResult, + saving, + saveError, + expectedFotaModels, + onConfirm, + onDismiss, +}: Props) { + if (!lookupResult) return null + + const model = (lookupResult.fotaModel ?? '').trim() + const modelMismatch = + expectedFotaModels.size > 0 && model !== '' && !expectedFotaModels.has(model) + + const statusLabel = getFotaActivityStatusLabel(lookupResult.fotaActivityStatus) + + const rows: Array<{ label: string; value: string | null }> = [ + { label: 'IMEI', value: lookupResult.imei }, + { label: 'Modele', value: lookupResult.fotaModel }, + { label: 'Serial', value: lookupResult.fotaSerial }, + { label: 'Firmware', value: lookupResult.fotaCurrentFirmware }, + { label: 'Statut', value: statusLabel }, + { label: 'Vu le', value: lookupResult.fotaSeenAt }, + { label: 'Organisation', value: lookupResult.fotaCompanyName }, + { label: 'Groupe', value: lookupResult.fotaGroupName }, + ] + + return ( + + + + Appareil detecte + + Informations FOTA de l'appareil scanne + + + +
+ {rows.map((row) => + row.value ? ( +
+ {row.label} + + {row.value} + +
+ ) : null + )} +
+ + {modelMismatch && ( + + + Modele FOTA ({model}) different du modele attendu ( + {Array.from(expectedFotaModels).join(', ')}) + + + )} + + {saveError && ( + + {saveError} + + )} + + + + + +
+
+ ) +} diff --git a/apps/web/src/components/admin/orders/NewOrderPage.tsx b/apps/web/src/components/admin/orders/NewOrderPage.tsx new file mode 100644 index 0000000..b4028fc --- /dev/null +++ b/apps/web/src/components/admin/orders/NewOrderPage.tsx @@ -0,0 +1,198 @@ +'use client' + +import Link from 'next/link' +import { useMemo, useState } from 'react' +import { ArrowLeft, Plus, Trash2, Check } from 'lucide-react' +import type { OrderCreate } from '@localiztoi/shared' + +import { api } from '@/config/api' +import { ensureErrorMessage } from '@/helpers/ensureErrorMessage' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Card, CardContent } from '@/components/ui/card' +import { Alert, AlertDescription } from '@/components/ui/alert' + +type ItemState = { + amazonSku: string + quantity: string + title: string +} + +const emptyItem: ItemState = { + amazonSku: '', + quantity: '1', + title: '' +} + +export const NewOrderPage = () => { + const [orderRef, setOrderRef] = useState('') + const [items, setItems] = useState([{ ...emptyItem }]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [createdId, setCreatedId] = useState(null) + + const canSubmit = useMemo(() => { + if (!orderRef.trim()) { + return false + } + + if (items.length === 0) { + return false + } + + return items.every((it) => it.amazonSku.trim() && Number(it.quantity) > 0) + }, [orderRef, items]) + + const setItem = (idx: number, patch: Partial) => { + setItems((prev) => prev.map((it, i) => (i === idx ? { ...it, ...patch } : it))) + } + + const addItem = () => { + setItems((prev) => [...prev, { ...emptyItem }]) + } + + const removeItem = (idx: number) => { + setItems((prev) => prev.filter((_it, i) => i !== idx)) + } + + const onCreate = async () => { + if (!canSubmit) { + return + } + + setLoading(true) + setError(null) + + try { + const payload: OrderCreate = { + orderRef: orderRef.trim(), + items: items.map((it) => ({ + amazonSku: it.amazonSku.trim(), + quantity: Number(it.quantity), + title: it.title.trim() ? it.title.trim() : undefined + })) + } + + const res = await api.post<{ id: string }>('/admin/orders', payload) + setCreatedId(res.data.id) + } catch (err) { + setError(ensureErrorMessage(err)) + } finally { + setLoading(false) + } + } + + return ( +
+
+ +

Créer une commande

+
+ +
+
+ + setOrderRef(e.target.value)} + placeholder="ex: AMAZON-ORDER-ID" + className="h-11" + /> +
+ +
+

Articles

+ + {items.map((it, idx) => ( + + + {/* Mobile: empilement vertical, Desktop: grille */} +
+
+ + setItem(idx, { amazonSku: e.target.value })} + className="h-11" + /> +
+ +
+ + setItem(idx, { quantity: e.target.value })} + type="number" + min={1} + step={1} + className="h-11" + /> +
+ +
+ + setItem(idx, { title: e.target.value })} + className="h-11" + /> +
+
+ + {items.length > 1 ? ( + + ) : null} +
+
+ ))} + + +
+ + + + {createdId ? ( + + + + Commande créée.{' '} + + Ouvrir la commande + + + + ) : null} + + {error ? ( + + {error} + + ) : null} +
+
+ ) +} diff --git a/apps/web/src/components/admin/orders/OrderDetailsPage.tsx b/apps/web/src/components/admin/orders/OrderDetailsPage.tsx new file mode 100644 index 0000000..c9dd93c --- /dev/null +++ b/apps/web/src/components/admin/orders/OrderDetailsPage.tsx @@ -0,0 +1,920 @@ +'use client' + +import Link from 'next/link' +import { useEffect, useMemo, useState } from 'react' +import { ArrowLeft, RefreshCw, Plus, Trash2, ScanLine, PackageMinus, Truck, ExternalLink, Send, User } from 'lucide-react' + +import { api } from '@/config/api' +import { ensureErrorMessage } from '@/helpers/ensureErrorMessage' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Badge } from '@/components/ui/badge' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Separator } from '@/components/ui/separator' +import { Skeleton } from '@/components/ui/skeleton' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' + +import type { OrderDetailsData } from './types' +import { getFotaActivityStatusLabel, extractColissimoTracking } from './types' + +const statusConfig: Record = { + new: { label: 'Nouvelle', variant: 'default' }, + processing: { label: 'En cours', variant: 'secondary' }, + ready: { label: 'Prête', variant: 'secondary' }, + shipped: { label: 'Expédiée', variant: 'outline' }, + done: { label: 'Terminée', variant: 'outline' }, +} + +export const OrderDetailsPage = ({ id }: { id: string }) => { + const [data, setData] = useState(null) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [scanError, setScanError] = useState(null) + const [manualImei, setManualImei] = useState('') + const [fotaMoveLoading, setFotaMoveLoading] = useState(false) + const [fotaMoveError, setFotaMoveError] = useState(null) + const [fotaMoveSuccess, setFotaMoveSuccess] = useState<{ backgroundActionId?: string | number } | null>(null) + const [destockLoading, setDestockLoading] = useState(false) + const [destockError, setDestockError] = useState(null) + const [destockSuccess, setDestockSuccess] = useState(false) + const [colissimoLoading, setColissimoLoading] = useState(false) + const [colissimoError, setColissimoError] = useState(null) + const [manualTracking, setManualTracking] = useState('') + const [amazonConfirmLoading, setAmazonConfirmLoading] = useState(false) + const [amazonConfirmError, setAmazonConfirmError] = useState(null) + const [amazonConfirmSuccess, setAmazonConfirmSuccess] = useState(false) + + + + const scannedImeis = useMemo(() => { + return new Set((data?.imeis ?? []).map((x) => x.imei)) + }, [data?.imeis]) + + const totalExpected = useMemo(() => { + return (data?.items ?? []).reduce((sum, it) => sum + it.quantity, 0) + }, [data?.items]) + + const expectedModels = useMemo(() => { + return new Set((data?.expectedFotaModels ?? []).map((x) => x.trim()).filter(Boolean)) + }, [data?.expectedFotaModels]) + + const mismatchedImeis = useMemo(() => { + if (!data) return [] + if (expectedModels.size === 0) return [] + + return data.imeis.filter((it) => { + const model = (it.fotaModel ?? '').trim() + if (!model) return true + return !expectedModels.has(model) + }) + }, [data, expectedModels]) + + const missingFotaInfoImeis = useMemo(() => { + if (!data) return [] + + return data.imeis.filter((it) => { + const model = (it.fotaModel ?? '').trim() + if (!model) return true + if (it.fotaLookupError) return true + return false + }) + }, [data]) + + const canMarkReady = useMemo(() => { + if (!data) return false + if (totalExpected <= 0) return false + if (data.imeis.length !== totalExpected) return false + if (missingFotaInfoImeis.length > 0) return false + if (mismatchedImeis.length > 0) return false + return true + }, [data, mismatchedImeis.length, missingFotaInfoImeis.length, totalExpected]) + + const load = async () => { + setLoading(true) + setError(null) + + try { + const res = await api.get(`/admin/orders/${id}`) + setData(res.data) + } catch (err) { + setError(ensureErrorMessage(err)) + } finally { + setLoading(false) + } + } + + const submitImei = async (raw: string) => { + const imei = raw.trim() + if (!imei) { + return + } + + setScanError(null) + + if (scannedImeis.has(imei)) { + setScanError('IMEI déjà scanné') + return + } + + try { + await api.post(`/admin/orders/${id}/scan-imei`, { imei }) + setManualImei('') + await load() + } catch (err) { + setScanError(ensureErrorMessage(err)) + } + } + + const deleteImei = async (imeiId: string) => { + setScanError(null) + try { + await api.delete(`/admin/orders/${id}/imeis/${imeiId}`) + await load() + } catch (err) { + setScanError(ensureErrorMessage(err)) + } + } + + const submitTracking = async (raw: string) => { + const rawTrimmed = raw.trim() + if (!rawTrimmed) return + + // Tenter d'extraire le numéro de suivi depuis un code-barres GeoLabel + const extracted = extractColissimoTracking(rawTrimmed) + const trackingNumber = extracted ?? rawTrimmed.replace(/\s/g, '') + + setColissimoError(null) + setColissimoLoading(true) + + try { + await api.post(`/admin/orders/${id}/tracking`, { trackingNumber }) + setManualTracking('') + await load() + } catch (err) { + setColissimoError(ensureErrorMessage(err)) + } finally { + setColissimoLoading(false) + } + } + + const deleteTracking = async () => { + setColissimoError(null) + setColissimoLoading(true) + + try { + await api.delete(`/admin/orders/${id}/tracking`) + await load() + } catch (err) { + const msg = ensureErrorMessage(err) + if (msg.includes('tracking_already_confirmed_on_amazon')) { + setColissimoError('Impossible de supprimer : le tracking a déjà été transmis à Amazon.') + } else { + setColissimoError(msg) + } + } finally { + setColissimoLoading(false) + } + } + + const confirmAmazonShipment = async () => { + setAmazonConfirmError(null) + setAmazonConfirmSuccess(false) + setAmazonConfirmLoading(true) + + try { + await api.post(`/admin/orders/${id}/amazon-confirm-shipment`) + setAmazonConfirmSuccess(true) + await load() + } catch (err) { + setAmazonConfirmError(ensureErrorMessage(err)) + } finally { + setAmazonConfirmLoading(false) + } + } + + const destockAxonaut = async () => { + setDestockError(null) + setDestockSuccess(false) + setDestockLoading(true) + + try { + await api.post(`/admin/orders/${id}/axonaut-destock`) + setDestockSuccess(true) + await load() + } catch (err) { + setDestockError(ensureErrorMessage(err)) + } finally { + setDestockLoading(false) + } + } + + const moveFotaDevices = async () => { + setFotaMoveError(null) + setFotaMoveSuccess(null) + setFotaMoveLoading(true) + + try { + const res = await api.post(`/admin/orders/${id}/fota-move`) + const backgroundActionId = (res.data as any)?.background_action_id ?? (res.data as any)?.backgroundActionId + setFotaMoveSuccess({ backgroundActionId }) + await load() + } catch (err) { + setFotaMoveError(ensureErrorMessage(err)) + } finally { + setFotaMoveLoading(false) + } + } + + useEffect(() => { + void load() + }, [id]) + + if (loading && !data) { + return ( +
+ + + +
+ ) + } + + return ( +
+ {/* Header */} +
+
+ +

Commande

+
+ +
+ + {error ? ( + + {error} + + ) : null} + + {data ? ( + <> + {/* Infos commande */} + + +
+

Référence

+

{data.orderRef}

+
+
+

Statut

+ + {statusConfig[data.status]?.label ?? data.status} + +
+
+

Progression

+

{data.imeis.length} / {totalExpected} IMEI

+
+
+
+ + {expectedModels.size > 0 && mismatchedImeis.length > 0 ? ( + + + Modèle FOTA différent du modèle attendu. +
+ Attendu: {(data.expectedFotaModels ?? []).join(', ')} +
+ IMEI concernés: {mismatchedImeis.map((x) => x.imei).join(', ')} +
+
+ ) : null} + + {missingFotaInfoImeis.length > 0 ? ( + + + Infos FOTA manquantes sur certains appareils (modèle non trouvé ou erreur FOTA). +
+ IMEI concernés: {missingFotaInfoImeis.map((x) => x.imei).join(', ')} +
+
+ ) : null} + + {/* Scanner */} + + + + + Scan IMEI / numéro de série + + + + + + + +
+

Saisie manuelle

+
+ setManualImei(e.target.value)} + placeholder="IMEI ou numéro de série" + className="h-11 flex-1" + onKeyDown={(e) => { + if (e.key === 'Enter') { + void submitImei(manualImei) + } + }} + /> + +
+
+ + {scanError ? ( + + {scanError} + + ) : null} +
+
+ + {/* IMEI scannés */} + + +
+ IMEI scannés ({data.imeis.length}) + + + + + + + Marquer la commande comme prête à expédier ? + + Cette action vérifie la quantité et le modèle, puis transfère les appareils dans Teltonika. + + + + Annuler + void moveFotaDevices()}> + Confirmer + + + + +
+
+ + {!canMarkReady ? ( + + + Pour passer en prêt à expédier, il faut : {data.imeis.length} / {totalExpected} scannés et aucun mismatch modèle. + + + ) : null} + {fotaMoveSuccess ? ( + + + Commande marquée prête à expédier. Transfert Teltonika en cours. + {typeof fotaMoveSuccess.backgroundActionId !== 'undefined' && fotaMoveSuccess.backgroundActionId !== null ? ( + <> +
+ Background action id: {String(fotaMoveSuccess.backgroundActionId)} + + ) : null} +
+
+ ) : null} + {fotaMoveError ? ( + + {fotaMoveError} + + ) : null} + {data.imeis.length === 0 ? ( +

Aucun IMEI scanné.

+ ) : ( + <> + {/* Mobile: cartes */} +
+ {data.imeis.map((it) => ( +
+
+

{it.imei}

+

+ {new Date(it.createdAt).toLocaleString()} +

+ {it.fotaLookupError ? ( +

FOTA: {it.fotaLookupError}

+ ) : ( +
+ {it.fotaModel ?

Modèle: {it.fotaModel}

: null} + {it.fotaCurrentFirmware ?

FW: {it.fotaCurrentFirmware}

: null} + {getFotaActivityStatusLabel(it.fotaActivityStatus) ? ( +

Statut: {getFotaActivityStatusLabel(it.fotaActivityStatus)}

+ ) : null} + {it.fotaCompanyName ?

Org: {it.fotaCompanyName}

: null} + {it.fotaGroupName ?

Groupe: {it.fotaGroupName}

: null} + {it.fotaSeenAt ?

Vu: {it.fotaSeenAt}

: null} +
+ )} +
+ + + + + + + Supprimer cet IMEI ? + + L'IMEI {it.imei} sera retiré de cette commande. + + + + Annuler + void deleteImei(it.id)}> + Supprimer + + + + +
+ ))} +
+ + {/* Desktop: table */} +
+ + + + IMEI + Scanné le + Actions + + + + {data.imeis.map((it) => ( + + +
+ {it.imei} + {it.fotaLookupError ? ( + FOTA: {it.fotaLookupError} + ) : ( + + {[ + it.fotaModel ? `Modèle: ${it.fotaModel}` : null, + it.fotaCurrentFirmware ? `FW: ${it.fotaCurrentFirmware}` : null, + getFotaActivityStatusLabel(it.fotaActivityStatus) + ? `Statut: ${getFotaActivityStatusLabel(it.fotaActivityStatus)}` + : null, + it.fotaCompanyName ? `Org: ${it.fotaCompanyName}` : null, + it.fotaGroupName ? `Groupe: ${it.fotaGroupName}` : null, + it.fotaSeenAt ? `Vu: ${it.fotaSeenAt}` : null + ] + .filter(Boolean) + .join(' • ')} + + )} +
+
+ + {new Date(it.createdAt).toLocaleString()} + + + + + + + + + Supprimer cet IMEI ? + + L'IMEI {it.imei} sera retiré de cette commande. + + + + Annuler + void deleteImei(it.id)}> + Supprimer + + + + + +
+ ))} +
+
+
+ + )} +
+
+ + {/* Destockage Axonaut */} + + +
+ + + Destockage Axonaut + + {data.axonautDestockedAt ? ( + + Destocké le {new Date(data.axonautDestockedAt).toLocaleString()} + + ) : ( + + + + + + + Destocker dans Axonaut ? + + Cette action va diminuer le stock dans Axonaut pour chaque SKU de cette commande et créer une note avec les numéros IMEI. + + + + Annuler + void destockAxonaut()}> + Confirmer le destockage + + + + + )} +
+
+ + {destockSuccess ? ( + + + Destockage Axonaut effectué. Stock mis à jour et note IMEI créée. + + + ) : null} + {destockError ? ( + + {destockError} + + ) : null} + {data.axonautDestockError && !destockError ? ( + + Dernière erreur : {data.axonautDestockError} + + ) : null} + {!data.axonautDestockedAt && !canMarkReady ? ( +

+ Tous les IMEI doivent être scannés et validés avant de pouvoir destocker. +

+ ) : null} + {data.axonautDestockedAt ? ( +

+ Le stock a été mis à jour dans Axonaut et une note avec les IMEI a été créée. +

+ ) : null} +
+
+ + {/* Adresse client */} + {data.shippingFetchedAt ? ( + + + + + Adresse de livraison + + + +
+ {data.shippingName ? ( +
+

Nom

+

{data.shippingName}

+
+ ) : null} + {data.shippingLine1 ? ( +
+

Adresse

+

{data.shippingLine1}

+ {data.shippingLine2 ?

{data.shippingLine2}

: null} + {data.shippingLine3 ?

{data.shippingLine3}

: null} +
+ ) : null} + {data.shippingCity || data.shippingZipCode ? ( +
+

Ville

+

{[data.shippingZipCode, data.shippingCity].filter(Boolean).join(' ')}

+
+ ) : null} + {data.shippingCountryCode ? ( +
+

Pays

+

{data.shippingCountryCode}

+
+ ) : null} + {data.shippingPhone ? ( +
+

Téléphone

+

{data.shippingPhone}

+
+ ) : null} +
+
+
+ ) : null} + + {/* Expédition — Numéro de suivi */} + + +
+ + + Expédition + + {data.colissimoShippedAt ? ( + + Expédié le {new Date(data.colissimoShippedAt).toLocaleString()} + + ) : null} +
+
+ + {data.colissimoTrackingNumber ? ( +
+
+

N° suivi

+

{data.colissimoTrackingNumber}

+
+
+ + {!data.amazonTrackingConfirmedAt ? ( + + + + + + + Supprimer le numéro de suivi ? + + Le numéro {data.colissimoTrackingNumber} sera dissocié de cette commande. Vous pourrez en scanner un nouveau. + + + + Annuler + void deleteTracking()}> + Supprimer + + + + + ) : null} +
+
+ ) : ( + <> + + + + +
+

Saisie manuelle

+
+ setManualTracking(e.target.value)} + placeholder="Ex: 6A12345678901" + className="h-11 flex-1 font-mono" + onKeyDown={(e) => { + if (e.key === 'Enter') { + void submitTracking(manualTracking) + } + }} + /> + +
+
+ + )} + + {colissimoError ? ( + + {colissimoError} + + ) : null} + {data.colissimoError && !colissimoError ? ( + + Dernière erreur : {data.colissimoError} + + ) : null} +
+
+ + {/* Confirmation Amazon */} + + +
+ + + Confirmation Amazon + + {data.amazonTrackingConfirmedAt ? ( + + Confirmé le {new Date(data.amazonTrackingConfirmedAt).toLocaleString()} + + ) : null} +
+
+ + {data.amazonTrackingConfirmedAt ? ( +

+ Le numéro de suivi Colissimo a été transmis à Amazon. Le client a été notifié automatiquement. +

+ ) : !data.colissimoTrackingNumber ? ( +

+ Un numéro de suivi doit être associé avant de confirmer l'expédition sur Amazon. +

+ ) : ( + <> +

+ Transmettre le numéro de suivi {data.colissimoTrackingNumber} à Amazon pour notifier le client. +

+ + + + + + + Confirmer l'expédition sur Amazon ? + + Le numéro de suivi Colissimo sera transmis à Amazon. Le client recevra une notification d'expédition avec le lien de suivi. + + + + Annuler + void confirmAmazonShipment()}> + Confirmer + + + + + + )} + + {amazonConfirmSuccess && !data.amazonTrackingConfirmedAt ? ( + + + Expédition confirmée sur Amazon. Le client a été notifié. + + + ) : null} + {amazonConfirmError ? ( + + {amazonConfirmError} + + ) : null} + {data.amazonTrackingError && !amazonConfirmError ? ( + + Dernière erreur : {data.amazonTrackingError} + + ) : null} +
+
+ + {/* Articles */} + + + Articles ({data.items.length}) + + + {/* Mobile: cartes */} +
+ {data.items.map((it) => ( +
+
+

{it.amazonSku}

+ {it.title ?

{it.title}

: null} +
+ x{it.quantity} +
+ ))} +
+ + {/* Desktop: table */} +
+ + + + Amazon SKU + Qté + Titre + + + + {data.items.map((it) => ( + + {it.amazonSku} + {it.quantity} + {it.title ?? ''} + + ))} + +
+
+
+
+ + ) : null} +
+ ) +} diff --git a/apps/web/src/components/admin/orders/OrdersListPage.tsx b/apps/web/src/components/admin/orders/OrdersListPage.tsx new file mode 100644 index 0000000..133fa68 --- /dev/null +++ b/apps/web/src/components/admin/orders/OrdersListPage.tsx @@ -0,0 +1,168 @@ +'use client' + +import Link from 'next/link' +import { useEffect, useState } from 'react' +import { RefreshCw, ChevronRight } from 'lucide-react' + +import { api } from '@/config/api' +import { ensureErrorMessage } from '@/helpers/ensureErrorMessage' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { Card, CardContent } from '@/components/ui/card' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { Skeleton } from '@/components/ui/skeleton' + +type OrderListItem = { + id: string + orderRef: string + status: string + createdAt: string + updatedAt: string +} + +const statusConfig: Record = { + new: { label: 'Nouvelle', variant: 'default' }, + processing: { label: 'En cours', variant: 'secondary' }, + ready: { label: 'Prête', variant: 'secondary' }, + shipped: { label: 'Expédiée', variant: 'outline' }, + done: { label: 'Terminée', variant: 'outline' }, +} + +const StatusBadge = ({ status }: { status: string }) => { + const config = statusConfig[status] ?? { label: status, variant: 'secondary' as const } + return {config.label} +} + +export const OrdersListPage = () => { + const [items, setItems] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + const load = async () => { + setLoading(true) + setError(null) + + try { + const res = await api.get('/admin/orders') + setItems(res.data) + } catch (err) { + setError(ensureErrorMessage(err)) + } finally { + setLoading(false) + } + } + + useEffect(() => { + void load() + }, []) + + return ( +
+
+

Commandes

+ +
+ + {error ? ( + + {error} + + ) : null} + + {/* Mobile : cartes */} +
+ {loading && items.length === 0 ? ( + <> + + + + + ) : items.length === 0 ? ( +

Aucune commande.

+ ) : ( + items.map((it) => ( + + + +
+

{it.orderRef}

+

+ {new Date(it.createdAt).toLocaleDateString()} +

+
+
+ + +
+
+
+ + )) + )} +
+ + {/* Desktop : table */} +
+ + + + Référence + Statut + Créée + Actions + + + + {loading && items.length === 0 ? ( + <> + {[1, 2, 3].map((i) => ( + + + + + + + ))} + + ) : items.length === 0 ? ( + + + Aucune commande. + + + ) : ( + items.map((it) => ( + + {it.orderRef} + + + {new Date(it.createdAt).toLocaleString()} + + + + + + )) + )} + +
+
+
+ ) +} diff --git a/apps/web/src/components/admin/orders/QrImeiScanner.tsx b/apps/web/src/components/admin/orders/QrImeiScanner.tsx new file mode 100644 index 0000000..15ca3da --- /dev/null +++ b/apps/web/src/components/admin/orders/QrImeiScanner.tsx @@ -0,0 +1,795 @@ +'use client' + +import { useEffect, useMemo, useRef, useState } from 'react' +import { Camera, CameraOff, SwitchCamera, RotateCw, Flashlight, FlashlightOff, Volume2, VolumeOff } from 'lucide-react' + +type Props = { + onDecoded: (text: string) => void + disabled?: boolean + autoStart?: boolean + scanMode?: 'imei' | 'tracking' | 'mixed' +} + +type CameraDevice = { + id: string + label?: string +} + +const scoreBackCamera = (label: string) => { + const l = label.toLowerCase() + let score = 0 + if (l.includes('back') || l.includes('rear') || l.includes('environment') || l.includes('arriere')) score += 100 + if (l.includes('wide') || l.includes('1x') || l.includes('main')) score += 20 + if (l.includes('ultra')) score -= 40 + if (l.includes('tele')) score -= 15 + if (l.includes('front') || l.includes('user') || l.includes('facetime')) score -= 120 + return score +} + +const isLikelyBackCamera = (label: string) => scoreBackCamera(label) >= 80 + +const playBeep = () => { + try { + const ctx = new (window.AudioContext || (window as any).webkitAudioContext)() + const osc = ctx.createOscillator() + const gain = ctx.createGain() + osc.connect(gain) + gain.connect(ctx.destination) + osc.frequency.value = 880 + gain.gain.value = 0.3 + osc.start() + osc.stop(ctx.currentTime + 0.12) + osc.onended = () => ctx.close() + } catch { + // Audio non supporte + } +} + +const iconBtnStyle: React.CSSProperties = { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: 40, + height: 40, + borderRadius: 10, + border: '1px solid #e0e0e0', + background: '#fff', + cursor: 'pointer', + transition: 'background 0.15s', +} + +const iconBtnActiveStyle: React.CSSProperties = { + ...iconBtnStyle, + background: '#18181b', + borderColor: '#18181b', + color: '#fff', +} + +const iconBtnDisabledStyle: React.CSSProperties = { + ...iconBtnStyle, + opacity: 0.35, + cursor: 'not-allowed', +} + +export const QrImeiScanner = ({ onDecoded, disabled, autoStart, scanMode = 'mixed' }: Props) => { + const isIOS = useMemo(() => { + if (typeof navigator === 'undefined') return false + const ua = navigator.userAgent || '' + const isIPhoneOrIPad = /iPhone|iPad|iPod/i.test(ua) + const isIPadDesktopUA = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1 + return isIPhoneOrIPad || isIPadDesktopUA + }, []) + + const [active, setActive] = useState(false) + const [error, setError] = useState(null) + const [infoMessage, setInfoMessage] = useState(null) + const [cameras, setCameras] = useState([]) + const [cameraIndex, setCameraIndex] = useState(0) + const [flip, setFlip] = useState(false) + const [torchOn, setTorchOn] = useState(false) + const [torchSupported, setTorchSupported] = useState(false) + const [beepEnabled, setBeepEnabled] = useState(true) + const [debugInfo, setDebugInfo] = useState({ + videoWidth: 0, + videoHeight: 0, + frames: 0, + decodingActive: false, + }) + + const beepEnabledRef = useRef(true) + beepEnabledRef.current = beepEnabled + + const lastDecodedRef = useRef(null) + const onDecodedRef = useRef(onDecoded) + onDecodedRef.current = onDecoded + + const autoStartedRef = useRef(false) + const userSelectedCameraRef = useRef(false) + const frameCounterRef = useRef(0) + const decodeCountRef = useRef(0) + const engineToggleDoneRef = useRef(false) + const engineToggleTimeoutRef = useRef(null) + const aliveLogIntervalRef = useRef(null) + const debugUiIntervalRef = useRef(null) + + useEffect(() => { + if (autoStart && !autoStartedRef.current) { + autoStartedRef.current = true + if (isIOS) { + setInfoMessage('iPhone: appuyez sur le bouton caméra pour démarrer') + return + } + setActive(true) + } + }, [autoStart, isIOS]) + + const regionId = useMemo(() => { + return `qr-reader-${Math.random().toString(16).slice(2)}` + }, []) + + useEffect(() => { + if (!active) { + lastDecodedRef.current = null + frameCounterRef.current = 0 + decodeCountRef.current = 0 + engineToggleDoneRef.current = false + if (engineToggleTimeoutRef.current) { + window.clearTimeout(engineToggleTimeoutRef.current) + engineToggleTimeoutRef.current = null + } + if (aliveLogIntervalRef.current) { + window.clearInterval(aliveLogIntervalRef.current) + aliveLogIntervalRef.current = null + } + if (debugUiIntervalRef.current) { + window.clearInterval(debugUiIntervalRef.current) + debugUiIntervalRef.current = null + } + setDebugInfo((prev) => ({ ...prev, decodingActive: false })) + return + } + + let cancelled = false + let qr: any + + const start = async () => { + setError(null) + setInfoMessage(null) + + try { + const mod = await import('html5-qrcode') + const Html5Qrcode = mod.Html5Qrcode + const Html5QrcodeSupportedFormats = mod.Html5QrcodeSupportedFormats + const ua = typeof navigator !== 'undefined' ? navigator.userAgent : '' + const isAppleMobile = ua.includes('iPhone') || ua.includes('iPad') + const hasBarcodeDetector = typeof window !== 'undefined' && 'BarcodeDetector' in window + + if (cancelled) return + decodeCountRef.current = 0 + engineToggleDoneRef.current = false + + const isLocalhost = + window.location.hostname === 'localhost' || + window.location.hostname === '127.0.0.1' || + window.location.hostname === '[::1]' + if (!window.isSecureContext && !isLocalhost) { + setError('Camera requires HTTPS on iPhone/mobile browsers') + setActive(false) + return + } + + const all = (await Html5Qrcode.getCameras()) as CameraDevice[] + + if (!cancelled) { + setCameras(all ?? []) + } + + // Premiere ouverture: preferer une camera arriere. + if (!cancelled && all?.length && cameraIndex === 0 && !userSelectedCameraRef.current) { + let bestIdx = 0 + let bestScore = Number.NEGATIVE_INFINITY + all.forEach((c, idx) => { + const score = scoreBackCamera(c.label ?? '') + if (score > bestScore) { + bestScore = score + bestIdx = idx + } + }) + if (bestIdx > 0) { + setCameraIndex(bestIdx) + return + } + } + + const onScan = (decodedText: string) => { + decodeCountRef.current += 1 + const cleaned = decodedText.trim() + if (!cleaned) return + if (lastDecodedRef.current === cleaned) return + + lastDecodedRef.current = cleaned + if (beepEnabledRef.current) playBeep() + onDecodedRef.current(cleaned) + + setTimeout(() => { + if (lastDecodedRef.current === cleaned) { + lastDecodedRef.current = null + } + }, 2000) + } + + const adaptiveQrbox = (viewfinderWidth: number, viewfinderHeight: number) => { + if (scanMode === 'imei') { + const scale = isAppleMobile ? 0.75 : 0.45 + const size = Math.floor(Math.min(viewfinderWidth, viewfinderHeight) * scale) + const clamped = Math.max(220, Math.min(size, 320)) + return { width: clamped, height: clamped } + } + + if (scanMode === 'tracking') { + const w = Math.floor(viewfinderWidth * 0.9) + const h = Math.floor(viewfinderHeight * 0.3) + return { width: Math.max(w, 260), height: Math.max(h, 90) } + } + + const w = Math.floor(viewfinderWidth * 0.9) + const h = Math.floor(viewfinderHeight * 0.35) + return { width: Math.max(w, 250), height: Math.max(h, 80) } + } + + const getFormatsToSupport = () => { + if (!Html5QrcodeSupportedFormats) return undefined + + if (scanMode === 'imei') { + return [ + Html5QrcodeSupportedFormats.QR_CODE, + Html5QrcodeSupportedFormats.DATA_MATRIX, + Html5QrcodeSupportedFormats.CODE_128, + Html5QrcodeSupportedFormats.EAN_13, + ] + } + + if (scanMode === 'tracking') { + return [ + Html5QrcodeSupportedFormats.CODE_128, + Html5QrcodeSupportedFormats.ITF, + Html5QrcodeSupportedFormats.EAN_13, + Html5QrcodeSupportedFormats.CODABAR, + Html5QrcodeSupportedFormats.QR_CODE, + ] + } + + return [ + Html5QrcodeSupportedFormats.QR_CODE, + Html5QrcodeSupportedFormats.DATA_MATRIX, + Html5QrcodeSupportedFormats.CODE_128, + Html5QrcodeSupportedFormats.CODE_39, + Html5QrcodeSupportedFormats.EAN_13, + Html5QrcodeSupportedFormats.ITF, + Html5QrcodeSupportedFormats.CODABAR, + Html5QrcodeSupportedFormats.PDF_417, + ] + } + + const qrbox = adaptiveQrbox + const formatsToSupport = getFormatsToSupport() + const defaultFps = scanMode === 'imei' ? (isAppleMobile ? 5 : 10) : 10 + const buildConfig = (useBarCodeDetectorIfSupported: boolean, fps: number) => ({ + fps, + qrbox, + experimentalFeatures: { useBarCodeDetectorIfSupported }, + formatsToSupport, + disableFlip: scanMode === 'imei' + }) as any + let currentUseBarcodeDetector = hasBarcodeDetector && !isAppleMobile + let config: any = buildConfig(currentUseBarcodeDetector, defaultFps) + let safeConfig: any = buildConfig(currentUseBarcodeDetector, 8) + const setEngineConfig = (useBarcodeDetector: boolean) => { + currentUseBarcodeDetector = useBarcodeDetector + config = buildConfig(currentUseBarcodeDetector, defaultFps) + safeConfig = buildConfig(currentUseBarcodeDetector, 8) + } + + qr = new Html5Qrcode(regionId) + const selected = all?.length ? all[Math.min(cameraIndex, all.length - 1)] : null + + const getIosVideoConstraints = () => { + if (!isAppleMobile) return undefined + if (selected?.id) { + return { + deviceId: { exact: selected.id }, + width: { ideal: 1920 }, + height: { ideal: 1080 }, + frameRate: { ideal: 15, max: 20 }, + } as MediaTrackConstraints + } + return { + facingMode: { exact: 'environment' }, + width: { ideal: 1920 }, + height: { ideal: 1080 }, + frameRate: { ideal: 15, max: 20 }, + } as MediaTrackConstraints + } + + if (isAppleMobile) { + const iosConstraints = getIosVideoConstraints() + if (iosConstraints) { + config = { ...config, videoConstraints: iosConstraints } + safeConfig = { ...safeConfig, videoConstraints: iosConstraints } + } + } + + const isConstraintError = (msg: string) => { + return msg.includes('Invalid constraint') || msg.includes('OverconstrainedError') + } + + const startOnBackCameraFirst = async () => { + if (userSelectedCameraRef.current) return false + const preferredCamera = all + ?.map((c, idx) => ({ c, idx, score: scoreBackCamera(c.label ?? '') })) + .sort((a, b) => b.score - a.score)?.[0] + + if (preferredCamera?.c?.id && isLikelyBackCamera(preferredCamera.c.label ?? '')) { + try { + await qr.start( + preferredCamera.c.id, + config, + onScan, + () => { + frameCounterRef.current += 1 + } + ) + return true + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + if (!isConstraintError(msg)) throw err + } + } + + try { + await qr.start( + { facingMode: 'environment' }, + config, + onScan, + () => { + frameCounterRef.current += 1 + } + ) + return true + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + if (!isConstraintError(msg)) throw err + } + return false + } + + const startWithCurrentConfig = async () => { + try { + if (isAppleMobile) { + try { + await qr.start( + selected?.id ?? { facingMode: 'environment' }, + config, + onScan, + () => { + frameCounterRef.current += 1 + } + ) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + if (!isConstraintError(msg)) throw err + try { + await qr.start( + selected?.id ?? { facingMode: 'environment' }, + safeConfig, + onScan, + () => { + frameCounterRef.current += 1 + } + ) + } catch { + await qr.start( + {}, + safeConfig, + onScan, + () => { + frameCounterRef.current += 1 + } + ) + } + } + } else { + const startedWithBackConstraint = await startOnBackCameraFirst() + if (startedWithBackConstraint) { + // nothing else + } else if (selected?.id) { + await qr.start( + selected.id, + config, + onScan, + () => { + frameCounterRef.current += 1 + } + ) + } else { + await qr.start( + { facingMode: 'environment' }, + config, + onScan, + () => { + frameCounterRef.current += 1 + } + ) + } + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + if (!isConstraintError(msg)) throw err + await qr.start( + {}, + safeConfig, + onScan, + () => { + frameCounterRef.current += 1 + } + ) + } + } + + await startWithCurrentConfig() + + const applyPreviewStyles = async () => { + const rootEl = document.getElementById(regionId) + const scannerChild = rootEl?.firstElementChild as HTMLElement | null + if (scannerChild) { + scannerChild.style.height = '100%' + scannerChild.style.minHeight = '100%' + } + + const videoEl = rootEl?.querySelector('video') as HTMLVideoElement | null + if (!videoEl) return + + videoEl.style.width = '100%' + videoEl.style.height = '100%' + videoEl.style.objectFit = isAppleMobile ? 'cover' : 'cover' + videoEl.style.display = 'block' + videoEl.setAttribute('playsinline', 'true') + videoEl.setAttribute('webkit-playsinline', 'true') + videoEl.muted = true + const shouldDisableTransform = isAppleMobile || scanMode === 'imei' + videoEl.style.transform = shouldDisableTransform ? '' : '' + videoEl.style.transformOrigin = 'center center' + + const stream = videoEl.srcObject as MediaStream | null + const track = stream?.getVideoTracks?.()?.[0] + if (!track) return + + const settings = track.getSettings?.() + const settingsWidth = typeof settings?.width === 'number' ? settings.width : undefined + const settingsHeight = typeof settings?.height === 'number' ? settings.height : undefined + if (!cancelled) { + setDebugInfo((prev) => ({ + ...prev, + videoWidth: settingsWidth ?? videoEl.videoWidth ?? 0, + videoHeight: settingsHeight ?? videoEl.videoHeight ?? 0, + })) + } + + { + // Demander plus de pixels quand possible (iOS ignore souvent, mais on tente). + void qr + .applyVideoConstraints({ + width: { ideal: isAppleMobile ? 1920 : 1920 }, + height: { ideal: isAppleMobile ? 1080 : 1080 }, + frameRate: { ideal: isAppleMobile ? 15 : 12, max: isAppleMobile ? 20 : 15 }, + }) + .catch(() => {}) + + const caps = track.getCapabilities?.() as + | { + focusMode?: string[] | string + zoom?: { min?: number; max?: number } + } + | undefined + const focusModes = Array.isArray(caps?.focusMode) + ? caps.focusMode + : typeof caps?.focusMode === 'string' + ? [caps.focusMode] + : [] + if (focusModes.includes('continuous')) { + void track + .applyConstraints({ + advanced: [{ focusMode: 'continuous' } as unknown as MediaTrackConstraintSet] + }) + .catch(() => {}) + } + + if (!isAppleMobile) { + const minZoom = typeof caps?.zoom?.min === 'number' ? caps.zoom.min : 1 + const maxZoom = typeof caps?.zoom?.max === 'number' ? caps.zoom.max : 1 + if (maxZoom > minZoom) { + const targetZoom = Math.min(Math.max(2, minZoom), maxZoom) + void track + .applyConstraints({ + advanced: [{ zoom: targetZoom } as unknown as MediaTrackConstraintSet] + }) + .catch(() => {}) + } + } + } + } + + const detectTorch = () => { + const videoEl = document.getElementById(regionId)?.querySelector('video') as HTMLVideoElement | null + if (!videoEl) return + const stream = videoEl.srcObject as MediaStream | null + const track = stream?.getVideoTracks?.()?.[0] + if (!track) return + const caps = track.getCapabilities?.() as any + if (caps?.torch && !cancelled) { + setTorchSupported(true) + } + } + + const scheduleIosEngineToggle = () => { + if (!isAppleMobile || engineToggleDoneRef.current) return + if (engineToggleTimeoutRef.current) { + window.clearTimeout(engineToggleTimeoutRef.current) + } + const framesAtStart = frameCounterRef.current + const decodeAtStart = decodeCountRef.current + engineToggleTimeoutRef.current = window.setTimeout(() => { + engineToggleTimeoutRef.current = null + void (async () => { + if (cancelled || engineToggleDoneRef.current) return + if (decodeCountRef.current > decodeAtStart) return + if (frameCounterRef.current <= framesAtStart) return + + engineToggleDoneRef.current = true + + try { + await qr.stop() + await qr.clear() + } catch { + // ignore + } + if (cancelled) return + + setEngineConfig(!currentUseBarcodeDetector) + qr = new Html5Qrcode(regionId) + + try { + await startWithCurrentConfig() + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + setError(msg) + return + } + + await applyPreviewStyles() + detectTorch() + })() + }, 1800) + } + + void applyPreviewStyles() + detectTorch() + setTimeout(() => { + void applyPreviewStyles() + detectTorch() + }, 300) + setTimeout(() => { + void applyPreviewStyles() + detectTorch() + }, 900) + scheduleIosEngineToggle() + + if (isAppleMobile) { + // Si iOS reste en 480x640, on relance avec contraintes plus strictes. + setTimeout(() => { + const videoEl = document.getElementById(regionId)?.querySelector('video') as HTMLVideoElement | null + const width = videoEl?.videoWidth ?? 0 + if (cancelled || width >= 720) return + void (async () => { + try { + await qr.stop() + await qr.clear() + } catch { + // ignore + } + if (cancelled) return + qr = new Html5Qrcode(regionId) + try { + await qr.start( + selected?.id ?? { facingMode: 'environment' }, + safeConfig, + onScan, + () => { + frameCounterRef.current += 1 + } + ) + } catch { + // ignore + } + })() + }, 1200) + } + + setDebugInfo((prev) => ({ ...prev, decodingActive: true })) + + aliveLogIntervalRef.current = window.setInterval(() => { + // Debug temporaire demandé + console.log('scanner alive') + }, 5000) + + debugUiIntervalRef.current = window.setInterval(() => { + const videoEl = document.getElementById(regionId)?.querySelector('video') as HTMLVideoElement | null + const stream = videoEl?.srcObject as MediaStream | null + const track = stream?.getVideoTracks?.()?.[0] + const settings = track?.getSettings?.() + const settingsWidth = typeof settings?.width === 'number' ? settings.width : undefined + const settingsHeight = typeof settings?.height === 'number' ? settings.height : undefined + setDebugInfo({ + videoWidth: settingsWidth ?? videoEl?.videoWidth ?? 0, + videoHeight: settingsHeight ?? videoEl?.videoHeight ?? 0, + frames: frameCounterRef.current, + decodingActive: true, + }) + }, 500) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + setError(msg) + } + } + + void start() + + return () => { + cancelled = true + setTorchOn(false) + setTorchSupported(false) + setDebugInfo((prev) => ({ ...prev, decodingActive: false })) + if (engineToggleTimeoutRef.current) { + window.clearTimeout(engineToggleTimeoutRef.current) + engineToggleTimeoutRef.current = null + } + if (aliveLogIntervalRef.current) { + window.clearInterval(aliveLogIntervalRef.current) + aliveLogIntervalRef.current = null + } + if (debugUiIntervalRef.current) { + window.clearInterval(debugUiIntervalRef.current) + debugUiIntervalRef.current = null + } + + const stop = async () => { + try { + if (qr) { + await qr.stop() + await qr.clear() + } + } catch { + // ignore + } + } + + void stop() + } + }, [active, cameraIndex, regionId, scanMode]) + + useEffect(() => { + const videoEl = document.getElementById(regionId)?.querySelector('video') as HTMLVideoElement | null + if (!videoEl) return + + const shouldDisableTransform = isIOS || scanMode === 'imei' + videoEl.style.transform = shouldDisableTransform ? '' : flip ? 'rotate(180deg)' : '' + videoEl.style.transformOrigin = 'center center' + }, [flip, isIOS, regionId, scanMode]) + + useEffect(() => { + if (!active) return + + const videoEl = document.getElementById(regionId)?.querySelector('video') as HTMLVideoElement | null + if (!videoEl) return + const stream = videoEl.srcObject as MediaStream | null + const track = stream?.getVideoTracks?.()?.[0] + if (!track) return + + try { + void track.applyConstraints({ advanced: [{ torch: torchOn } as any] }) + } catch { + // ignore + } + }, [torchOn, active, regionId]) + + const cycleCamera = () => { + if (cameras.length <= 1) return + userSelectedCameraRef.current = true + setCameraIndex((i) => (i + 1) % cameras.length) + } + + const toggleActive = () => { + setInfoMessage(null) + setActive((v) => !v) + } + + return ( +
+
+ + + + + + + + + +
+ + {error ?
Erreur camera: {error}
: null} + {infoMessage ?
{infoMessage}
: null} +
+ debug video: {debugInfo.videoWidth}x{debugInfo.videoHeight} +
+
+ debug frames: {debugInfo.frames} | decoding: {debugInfo.decodingActive ? 'oui' : 'non'} +
+ +
+
+ ) +} diff --git a/apps/web/src/components/admin/orders/ScanPage.tsx b/apps/web/src/components/admin/orders/ScanPage.tsx new file mode 100644 index 0000000..7cea6c8 --- /dev/null +++ b/apps/web/src/components/admin/orders/ScanPage.tsx @@ -0,0 +1,259 @@ +'use client' + +import Link from 'next/link' +import { useRouter } from 'next/navigation' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { ArrowLeft, Check, Loader2 } from 'lucide-react' + +import { api } from '@/config/api' +import { ensureErrorMessage } from '@/helpers/ensureErrorMessage' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Skeleton } from '@/components/ui/skeleton' + +import { QrImeiScanner } from './QrImeiScanner' +import { ImeiValidationModal } from './ImeiValidationModal' +import type { OrderDetailsData, FotaLookupResult } from './types' + +type Props = { id: string } + +export function ScanPage({ id }: Props) { + const router = useRouter() + + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + const [lookupPending, setLookupPending] = useState(false) + const [lookupResult, setLookupResult] = useState(null) + const [lookupError, setLookupError] = useState(null) + const [modalOpen, setModalOpen] = useState(false) + const [saving, setSaving] = useState(false) + const [saveError, setSaveError] = useState(null) + + const busyRef = useRef(false) + + const load = useCallback(async () => { + try { + setError(null) + const res = await api.get(`/admin/orders/${id}`) + setData(res.data as OrderDetailsData) + } catch (err) { + setError(ensureErrorMessage(err)) + } finally { + setLoading(false) + } + }, [id]) + + useEffect(() => { + void load() + }, [load]) + + const scannedImeis = useMemo(() => { + return new Set((data?.imeis ?? []).map((x) => x.imei)) + }, [data?.imeis]) + + const totalExpected = useMemo(() => { + return (data?.items ?? []).reduce((sum, it) => sum + it.quantity, 0) + }, [data?.items]) + + const expectedModels = useMemo(() => { + return new Set( + (data?.expectedFotaModels ?? []).map((x) => x.trim()).filter(Boolean) + ) + }, [data?.expectedFotaModels]) + + const handleDecoded = useCallback( + async (text: string) => { + if (busyRef.current || modalOpen) return + const cleaned = text.trim() + if (!cleaned) return + + if (scannedImeis.has(cleaned)) { + setLookupError('IMEI deja scanne sur cette commande') + setTimeout(() => setLookupError(null), 3000) + return + } + + busyRef.current = true + setLookupPending(true) + setLookupError(null) + setSaveError(null) + + try { + const res = await api.get('/admin/fota/lookup', { + params: { identifier: cleaned }, + }) + const result = res.data as FotaLookupResult + + if (scannedImeis.has(result.imei)) { + setLookupError('IMEI deja scanne sur cette commande') + setTimeout(() => setLookupError(null), 3000) + return + } + + setLookupResult(result) + setModalOpen(true) + } catch (err) { + setLookupError(ensureErrorMessage(err)) + setTimeout(() => setLookupError(null), 5000) + } finally { + setLookupPending(false) + busyRef.current = false + } + }, + [modalOpen, scannedImeis] + ) + + const handleConfirm = useCallback(async () => { + if (!lookupResult) return + setSaving(true) + setSaveError(null) + + try { + await api.post(`/admin/orders/${id}/scan-imei`, { + imei: lookupResult.imei, + }) + + setModalOpen(false) + setLookupResult(null) + + const res = await api.get(`/admin/orders/${id}`) + const updated = res.data as OrderDetailsData + setData(updated) + + const newCount = updated.imeis.length + const expected = updated.items.reduce((sum, it) => sum + it.quantity, 0) + if (newCount >= expected) { + router.push(`/admin/orders/${id}`) + } + } catch (err) { + setSaveError(ensureErrorMessage(err)) + } finally { + setSaving(false) + } + }, [id, lookupResult, router]) + + const handleDismiss = useCallback(() => { + setModalOpen(false) + setLookupResult(null) + setSaveError(null) + }, []) + + if (loading) { + return ( +
+ + + +
+ ) + } + + if (error || !data) { + return ( +
+ + + {error ?? 'Commande introuvable'} + +
+ ) + } + + const scannedCount = data.imeis.length + + return ( +
+ {/* Header */} +
+
+ +
+

Scan IMEI

+

{data.orderRef}

+
+
+ = totalExpected ? 'default' : 'secondary'} className="text-sm"> + {scannedCount} / {totalExpected} IMEI + +
+ +
+ {/* Camera */} +
+ void handleDecoded(text)} + disabled={lookupPending || saving} + autoStart + scanMode="imei" + /> + {lookupPending && ( +
+ +
+ )} +
+ + {/* Liste des IMEI scannes */} +
+ {lookupError && ( + + {lookupError} + + )} + +
+ {scannedCount === 0 ? ( +

+ Aucun IMEI scanne +

+ ) : ( + data.imeis.map((imei) => ( +
+
+

{imei.imei}

+

+ {[imei.fotaModel, imei.fotaSerial].filter(Boolean).join(' - ')} +

+
+ +
+ )) + )} +
+
+
+ + {/* Modale de validation */} + void handleConfirm()} + onDismiss={handleDismiss} + /> +
+ ) +} diff --git a/apps/web/src/components/admin/orders/ScanTrackingPage.tsx b/apps/web/src/components/admin/orders/ScanTrackingPage.tsx new file mode 100644 index 0000000..4cff589 --- /dev/null +++ b/apps/web/src/components/admin/orders/ScanTrackingPage.tsx @@ -0,0 +1,245 @@ +'use client' + +import Link from 'next/link' +import { useRouter } from 'next/navigation' +import { useCallback, useEffect, useState } from 'react' +import { ArrowLeft, Loader2, Package } from 'lucide-react' + +import { api } from '@/config/api' +import { ensureErrorMessage } from '@/helpers/ensureErrorMessage' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Skeleton } from '@/components/ui/skeleton' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' + +import { QrImeiScanner } from './QrImeiScanner' +import type { OrderDetailsData } from './types' +import { extractColissimoTracking } from './types' + +type Props = { id: string } + +export function ScanTrackingPage({ id }: Props) { + const router = useRouter() + + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + const [scannedValue, setScannedValue] = useState(null) + const [trackingNumber, setTrackingNumber] = useState(null) + const [modalOpen, setModalOpen] = useState(false) + const [saving, setSaving] = useState(false) + const [saveError, setSaveError] = useState(null) + const [scanError, setScanError] = useState(null) + + const load = useCallback(async () => { + try { + setError(null) + const res = await api.get(`/admin/orders/${id}`) + setData(res.data as OrderDetailsData) + } catch (err) { + setError(ensureErrorMessage(err)) + } finally { + setLoading(false) + } + }, [id]) + + useEffect(() => { + void load() + }, [load]) + + const handleDecoded = useCallback( + (text: string) => { + if (modalOpen || saving) return + const raw = text.trim() + if (!raw) return + + const extracted = extractColissimoTracking(raw) + const tracking = extracted ?? raw.replace(/\s/g, '') + + if (!tracking) { + setScanError('Code-barres non reconnu') + setTimeout(() => setScanError(null), 3000) + return + } + + setScanError(null) + setSaveError(null) + setScannedValue(raw) + setTrackingNumber(tracking) + setModalOpen(true) + }, + [modalOpen, saving] + ) + + const handleConfirm = useCallback(async () => { + if (!trackingNumber) return + setSaving(true) + setSaveError(null) + + try { + await api.post(`/admin/orders/${id}/tracking`, { trackingNumber }) + setModalOpen(false) + setTrackingNumber(null) + setScannedValue(null) + router.push(`/admin/orders/${id}`) + } catch (err) { + setSaveError(ensureErrorMessage(err)) + } finally { + setSaving(false) + } + }, [id, trackingNumber, router]) + + const handleDismiss = useCallback(() => { + setModalOpen(false) + setTrackingNumber(null) + setScannedValue(null) + setSaveError(null) + }, []) + + if (loading) { + return ( +
+ + +
+ ) + } + + if (error || !data) { + return ( +
+ + + {error ?? 'Commande introuvable'} + +
+ ) + } + + const hasTracking = !!data.colissimoTrackingNumber + + return ( +
+ {/* Header */} +
+
+ +
+

Scan Colissimo

+

{data.orderRef}

+
+
+ {hasTracking ? ( + + + {data.colissimoTrackingNumber} + + ) : ( + + Pas de suivi + + )} +
+ + {/* Camera */} +
+ + {saving && ( +
+ +
+ )} +
+ + {/* Erreur scan inline */} + {scanError && ( + + {scanError} + + )} + + {/* Info si tracking deja associe */} + {hasTracking && ( + + + Un numéro de suivi est déjà associé à cette commande : {data.colissimoTrackingNumber} + + + )} + + {/* Modale de confirmation */} + + + + Numéro de suivi détecté + + Confirmez le numéro de suivi scanné + + + +
+
+ N° suivi + {trackingNumber} +
+ {scannedValue && scannedValue !== trackingNumber && ( +
+ Code brut + {scannedValue} +
+ )} +
+ + {saveError && ( + + {saveError} + + )} + + + + Recommencer + + void handleConfirm()} disabled={saving}> + {saving ? ( + <> + + Validation... + + ) : ( + 'Valider' + )} + + +
+
+
+ ) +} diff --git a/apps/web/src/components/admin/orders/types.ts b/apps/web/src/components/admin/orders/types.ts new file mode 100644 index 0000000..8207d57 --- /dev/null +++ b/apps/web/src/components/admin/orders/types.ts @@ -0,0 +1,108 @@ +export type OrderItem = { + id: string + amazonSku: string + quantity: number + title: string | null + expectedFotaModel?: string | null +} + +export type OrderImei = { + id: string + imei: string + createdAt: string + fotaModel?: string | null + fotaSerial?: string | null + fotaCurrentFirmware?: string | null + fotaActivityStatus?: number | null + fotaSeenAt?: string | null + fotaCompanyName?: string | null + fotaGroupName?: string | null + fotaLookupError?: string | null + fotaLastLookupAt?: string | null + fotaMovedAt?: string | null + fotaMoveError?: string | null +} + +export type OrderDetailsData = { + id: string + orderRef: string + status: string + createdAt: string + updatedAt: string + axonautDestockedAt?: string | null + axonautDestockError?: string | null + colissimoTrackingNumber?: string | null + colissimoLabelUrl?: string | null + colissimoShippedAt?: string | null + colissimoError?: string | null + amazonTrackingConfirmedAt?: string | null + amazonTrackingError?: string | null + shippingName?: string | null + shippingFirstName?: string | null + shippingLastName?: string | null + shippingLine1?: string | null + shippingLine2?: string | null + shippingLine3?: string | null + shippingCity?: string | null + shippingZipCode?: string | null + shippingCountryCode?: string | null + shippingPhone?: string | null + shippingFetchedAt?: string | null + shippingFetchError?: string | null + items: OrderItem[] + expectedFotaModels?: string[] + imeis: OrderImei[] +} + +export type FotaLookupResult = { + imei: string + fotaModel: string | null + fotaSerial: string | null + fotaCurrentFirmware: string | null + fotaActivityStatus: number | null + fotaSeenAt: string | null + fotaCompanyName: string | null + fotaGroupName: string | null +} + +/** + * Extrait le numéro de suivi Colissimo 13 caractères depuis un code-barres GeoLabel La Poste. + * Format GS1-128 : FNC1 (%) + 7 CP dest + 2 routage + 2 code produit + 10 identifiant + 3 service + 3 pays + * Retourne null si le format ne correspond pas. + */ +export const extractColissimoTracking = (barcodeScan: string): string | null => { + const data = barcodeScan.replace(/^(%|\]C1|\x1d)/, '') + if (data.length < 21) return null + + const productCode = data.substring(9, 11) + const parcelId = data.substring(11, 21) + + if (!/^[0-9A-Z]{2}$/.test(productCode)) return null + if (!/^\d{10}$/.test(parcelId)) return null + + const reversed = parcelId.split('').reverse() + let sumOdd = 0 + let sumEven = 0 + for (let i = 0; i < reversed.length; i++) { + const d = parseInt(reversed[i], 10) + if (i % 2 === 0) sumOdd += d + else sumEven += d + } + const total = sumOdd * 3 + sumEven + const checkResult = (Math.floor(total / 10) + 1) * 10 - total + const checkDigit = checkResult === 10 ? 0 : checkResult + + return productCode + parcelId + checkDigit +} + +export const getFotaActivityStatusLabel = (status?: number | null): string | null => { + if (status === null || typeof status === 'undefined') { + return null + } + + if (status === 2) return 'En ligne' + if (status === 1) return 'Hors ligne' + if (status === 0) return 'Inactif' + + return String(status) +} diff --git a/apps/web/src/components/auth/LoginPage.tsx b/apps/web/src/components/auth/LoginPage.tsx new file mode 100644 index 0000000..8a77c27 --- /dev/null +++ b/apps/web/src/components/auth/LoginPage.tsx @@ -0,0 +1,94 @@ +'use client' + +import { useState } from 'react' +import { MapPin } from 'lucide-react' + +import { api } from '@/config/api' +import { ensureErrorMessage } from '@/helpers/ensureErrorMessage' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Alert, AlertDescription } from '@/components/ui/alert' + +export const LoginPage = () => { + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + setError(null) + + try { + await api.post('/auth/login', { username, password }) + window.location.href = '/admin' + } catch (err) { + const msg = ensureErrorMessage(err) + if (msg.includes('invalid_credentials')) { + setError('Nom d\'utilisateur ou mot de passe incorrect.') + } else { + setError(msg) + } + } finally { + setLoading(false) + } + } + + return ( +
+ + +
+ + Localiztoi +
+ Connexion +
+ +
void handleSubmit(e)} className="grid gap-4"> + {error ? ( + + {error} + + ) : null} + +
+ + setUsername(e.target.value)} + autoCapitalize="none" + autoComplete="username" + autoCorrect="off" + className="h-12" + required + /> +
+ +
+ + setPassword(e.target.value)} + autoComplete="current-password" + className="h-12" + required + /> +
+ + +
+
+
+
+ ) +} diff --git a/apps/web/src/components/ui/alert-dialog.tsx b/apps/web/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..034b93f --- /dev/null +++ b/apps/web/src/components/ui/alert-dialog.tsx @@ -0,0 +1,196 @@ +"use client" + +import * as React from "react" +import { AlertDialog as AlertDialogPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + size = "default", + ...props +}: React.ComponentProps & { + size?: "default" | "sm" +}) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogMedia({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogAction({ + className, + variant = "default", + size = "default", + ...props +}: React.ComponentProps & + Pick, "variant" | "size">) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + variant = "outline", + size = "default", + ...props +}: React.ComponentProps & + Pick, "variant" | "size">) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogMedia, + AlertDialogOverlay, + AlertDialogPortal, + AlertDialogTitle, + AlertDialogTrigger, +} diff --git a/apps/web/src/components/ui/alert.tsx b/apps/web/src/components/ui/alert.tsx new file mode 100644 index 0000000..1421354 --- /dev/null +++ b/apps/web/src/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/apps/web/src/components/ui/badge.tsx b/apps/web/src/components/ui/badge.tsx new file mode 100644 index 0000000..beb56ed --- /dev/null +++ b/apps/web/src/components/ui/badge.tsx @@ -0,0 +1,48 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + link: "text-primary underline-offset-4 [a&]:hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant = "default", + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot.Root : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx new file mode 100644 index 0000000..b5ea4ab --- /dev/null +++ b/apps/web/src/components/ui/button.tsx @@ -0,0 +1,64 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot.Root : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/apps/web/src/components/ui/card.tsx b/apps/web/src/components/ui/card.tsx new file mode 100644 index 0000000..681ad98 --- /dev/null +++ b/apps/web/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/apps/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx new file mode 100644 index 0000000..8916905 --- /dev/null +++ b/apps/web/src/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +export { Input } diff --git a/apps/web/src/components/ui/label.tsx b/apps/web/src/components/ui/label.tsx new file mode 100644 index 0000000..1ac80f7 --- /dev/null +++ b/apps/web/src/components/ui/label.tsx @@ -0,0 +1,24 @@ +"use client" + +import * as React from "react" +import { Label as LabelPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Label({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Label } diff --git a/apps/web/src/components/ui/separator.tsx b/apps/web/src/components/ui/separator.tsx new file mode 100644 index 0000000..4c24b2a --- /dev/null +++ b/apps/web/src/components/ui/separator.tsx @@ -0,0 +1,28 @@ +"use client" + +import * as React from "react" +import { Separator as SeparatorPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Separator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Separator } diff --git a/apps/web/src/components/ui/sheet.tsx b/apps/web/src/components/ui/sheet.tsx new file mode 100644 index 0000000..5963090 --- /dev/null +++ b/apps/web/src/components/ui/sheet.tsx @@ -0,0 +1,143 @@ +"use client" + +import * as React from "react" +import { XIcon } from "lucide-react" +import { Dialog as SheetPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Sheet({ ...props }: React.ComponentProps) { + return +} + +function SheetTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function SheetClose({ + ...props +}: React.ComponentProps) { + return +} + +function SheetPortal({ + ...props +}: React.ComponentProps) { + return +} + +function SheetOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetContent({ + className, + children, + side = "right", + showCloseButton = true, + ...props +}: React.ComponentProps & { + side?: "top" | "right" | "bottom" | "left" + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SheetTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Sheet, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} diff --git a/apps/web/src/components/ui/skeleton.tsx b/apps/web/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..32ea0ef --- /dev/null +++ b/apps/web/src/components/ui/skeleton.tsx @@ -0,0 +1,13 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Skeleton } diff --git a/apps/web/src/components/ui/sonner.tsx b/apps/web/src/components/ui/sonner.tsx new file mode 100644 index 0000000..9b20afe --- /dev/null +++ b/apps/web/src/components/ui/sonner.tsx @@ -0,0 +1,40 @@ +"use client" + +import { + CircleCheckIcon, + InfoIcon, + Loader2Icon, + OctagonXIcon, + TriangleAlertIcon, +} from "lucide-react" +import { useTheme } from "next-themes" +import { Toaster as Sonner, type ToasterProps } from "sonner" + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme() + + return ( + , + info: , + warning: , + error: , + loading: , + }} + style={ + { + "--normal-bg": "var(--popover)", + "--normal-text": "var(--popover-foreground)", + "--normal-border": "var(--border)", + "--border-radius": "var(--radius)", + } as React.CSSProperties + } + {...props} + /> + ) +} + +export { Toaster } diff --git a/apps/web/src/components/ui/table.tsx b/apps/web/src/components/ui/table.tsx new file mode 100644 index 0000000..51b74dd --- /dev/null +++ b/apps/web/src/components/ui/table.tsx @@ -0,0 +1,116 @@ +"use client" + +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Table({ className, ...props }: React.ComponentProps<"table">) { + return ( +
+ + + ) +} + +function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { + return ( + + ) +} + +function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { + return ( + + ) +} + +function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { + return ( + tr]:last:border-b-0", + className + )} + {...props} + /> + ) +} + +function TableRow({ className, ...props }: React.ComponentProps<"tr">) { + return ( + + ) +} + +function TableHead({ className, ...props }: React.ComponentProps<"th">) { + return ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ) +} + +function TableCell({ className, ...props }: React.ComponentProps<"td">) { + return ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ) +} + +function TableCaption({ + className, + ...props +}: React.ComponentProps<"caption">) { + return ( +
+ ) +} + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/apps/web/src/config/api.ts b/apps/web/src/config/api.ts new file mode 100644 index 0000000..d2f23ee --- /dev/null +++ b/apps/web/src/config/api.ts @@ -0,0 +1,20 @@ +import axios from 'axios' + +export const api = axios.create({ + baseURL: '/api', + withCredentials: true +}) + +api.interceptors.response.use( + (response) => response, + (error) => { + if ( + error?.response?.status === 401 && + typeof window !== 'undefined' && + !window.location.pathname.startsWith('/login') + ) { + window.location.href = '/login' + } + return Promise.reject(error) + } +) diff --git a/apps/web/src/helpers/ensureErrorMessage.ts b/apps/web/src/helpers/ensureErrorMessage.ts new file mode 100644 index 0000000..ad47d7a --- /dev/null +++ b/apps/web/src/helpers/ensureErrorMessage.ts @@ -0,0 +1,22 @@ +export const ensureErrorMessage = (err: unknown) => { + // Axios errors : extraire le message de la réponse API + const axiosData = (err as { response?: { data?: { message?: string; error?: string } } })?.response?.data + if (axiosData) { + if (typeof axiosData.message === 'string') return axiosData.message + if (typeof axiosData.error === 'string') return axiosData.error + } + + if (err instanceof Error) { + return err.message + } + + if (typeof err === 'string') { + return err + } + + try { + return JSON.stringify(err) + } catch { + return 'Unknown error' + } +} diff --git a/apps/web/src/hooks/useAuth.ts b/apps/web/src/hooks/useAuth.ts new file mode 100644 index 0000000..c0ff268 --- /dev/null +++ b/apps/web/src/hooks/useAuth.ts @@ -0,0 +1,36 @@ +'use client' + +import { useEffect, useState } from 'react' +import type { AuthUser } from '@localiztoi/shared' +import { api } from '@/config/api' + +export const useAuth = () => { + const [user, setUser] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + const check = async () => { + try { + const res = await api.get<{ user: AuthUser }>('/auth/me') + setUser(res.data.user) + } catch { + // L'interceptor axios gère la redirection vers /login en cas de 401 + } finally { + setLoading(false) + } + } + + void check() + }, []) + + const logout = async () => { + try { + await api.post('/auth/logout') + } catch { + // ignore + } + window.location.href = '/login' + } + + return { user, loading, logout } +} diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/apps/web/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..cf9c65d --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.mts" + ], + "exclude": ["node_modules"] +} diff --git a/apps/worker/dist/index.js b/apps/worker/dist/index.js new file mode 100644 index 0000000..2ce819a --- /dev/null +++ b/apps/worker/dist/index.js @@ -0,0 +1,10 @@ +import { Worker } from 'bullmq'; +const connection = { + host: process.env.REDIS_HOST ?? '127.0.0.1', + port: Number(process.env.REDIS_PORT ?? 6379) +}; +new Worker('default', async (job) => { + return { ok: true, name: job.name, data: job.data }; +}, { connection }); +// eslint-disable-next-line no-console +console.log('Worker started'); diff --git a/apps/worker/package.json b/apps/worker/package.json new file mode 100644 index 0000000..927791b --- /dev/null +++ b/apps/worker/package.json @@ -0,0 +1,19 @@ +{ + "name": "@localiztoi/worker", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc -p tsconfig.json", + "start": "node dist/index.js", + "lint": "echo \"lint not configured yet\"" + }, + "dependencies": { + "bullmq": "^5.69.3" + }, + "devDependencies": { + "@types/node": "^25.2.3", + "tsx": "^4.21.0", + "typescript": "^5.9.3" + } +} diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts new file mode 100644 index 0000000..0a251fc --- /dev/null +++ b/apps/worker/src/index.ts @@ -0,0 +1,17 @@ +import { Worker } from 'bullmq' + +const connection = { + host: process.env.REDIS_HOST ?? '127.0.0.1', + port: Number(process.env.REDIS_PORT ?? 6379) +} + +new Worker( + 'default', + async (job) => { + return { ok: true, name: job.name, data: job.data } + }, + { connection } +) + +// eslint-disable-next-line no-console +console.log('Worker started') diff --git a/apps/worker/tsconfig.json b/apps/worker/tsconfig.json new file mode 100644 index 0000000..73bc553 --- /dev/null +++ b/apps/worker/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "types": ["node"], + "resolveJsonModule": true + }, + "include": ["src"] +} diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..19428aa --- /dev/null +++ b/deploy.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -e + +echo "=== Localiztoi Stock — Déploiement ===" + +# 1. Pull le code +echo "[1/6] Pull du code..." +git pull origin main + +# 2. Installer les dépendances +echo "[2/6] Installation des dépendances..." +pnpm install --frozen-lockfile + +# 3. Build shared (nécessaire avant API et web) +echo "[3/6] Build shared..." +pnpm -F @localiztoi/shared build + +# 4. Build API +echo "[4/6] Build API..." +pnpm -F @localiztoi/api build + +# 5. Build frontend +echo "[5/6] Build frontend..." +pnpm -F web build + +# 6. Appliquer les migrations DB +echo "[6/6] Migrations DB..." +pnpm -F @localiztoi/api db:migrate + +# 7. Restart PM2 +echo "Redémarrage PM2..." +pm2 reload ecosystem.config.cjs + +echo "=== Déploiement terminé ===" +pm2 status diff --git a/docs/amazon-sp-api.md b/docs/amazon-sp-api.md new file mode 100644 index 0000000..3967f05 --- /dev/null +++ b/docs/amazon-sp-api.md @@ -0,0 +1,203 @@ +# Amazon SP-API — Documentation de référence + +## Authentification LWA (Login with Amazon) + +OAuth2 standard. 3 credentials nécessaires : +- `client_id` (LWA Client ID) — depuis Seller Central > Developer +- `client_secret` (LWA Client Secret) — expire tous les 180 jours +- `refresh_token` — obtenu après auto-autorisation de l'app privée + +### Obtenir un access_token + +```http +POST https://api.amazon.com/auth/o2/token +Content-Type: application/x-www-form-urlencoded + +grant_type=refresh_token +&refresh_token=Atzr|... +&client_id=amzn1.application-oa2-client.xxx +&client_secret=xxx +``` + +Réponse : +```json +{ + "access_token": "Atza|...", + "token_type": "bearer", + "expires_in": 3600 +} +``` + +L'access_token expire après **1 heure**. + +## Endpoints + +**Base URL Europe :** `https://sellingpartnerapi-eu.amazon.com` +**URL LWA (globale) :** `https://api.amazon.com/auth/o2/token` + +## Marketplace IDs + +| Pays | MarketplaceId | +|------|--------------| +| **France** | **`A13V1IB3VIYZZH`** | +| Allemagne | `A1PA6795UKMFR9` | +| Italie | `APJ6JRA9NG5V4` | +| Espagne | `A1RKKUPIHCS9HS` | +| Belgique | `AMEN7PMS3EDWL` | + +## Orders API + +### GET /orders/v0/orders + +Récupère la liste des commandes. + +**Paramètres obligatoires :** +- `MarketplaceIds` — ex: `A13V1IB3VIYZZH` +- `CreatedAfter` OU `LastUpdatedAfter` — date ISO 8601 + +**Paramètres optionnels :** +- `OrderStatuses` — `Unshipped`, `PartiallyShipped`, `Shipped`, `Pending`, `Canceled`... +- `FulfillmentChannels` — `MFN` (expédié par le vendeur) ou `AFN` (FBA) +- `MaxResultsPerPage` — défaut 100 +- `NextToken` — pagination + +**Réponse :** `response.payload.Orders` (tableau) + +### GET /orders/v0/orders/{orderId}/orderItems + +Récupère les articles d'une commande. + +**Réponse :** `response.payload.OrderItems` (tableau) + +## Structure des données + +### Order +```json +{ + "AmazonOrderId": "123-4567890-1234567", + "PurchaseDate": "2026-02-18T14:30:00Z", + "LastUpdateDate": "2026-02-18T15:00:00Z", + "OrderStatus": "Unshipped", + "FulfillmentChannel": "MFN", + "SalesChannel": "Amazon.fr", + "OrderTotal": { "CurrencyCode": "EUR", "Amount": "129.99" }, + "NumberOfItemsShipped": 0, + "NumberOfItemsUnshipped": 2, + "MarketplaceId": "A13V1IB3VIYZZH", + "EarliestShipDate": "2026-02-19T00:00:00Z", + "LatestShipDate": "2026-02-21T23:59:59Z" +} +``` + +### OrderItem +```json +{ + "ASIN": "B09XXXXXXXXX", + "SellerSKU": "FMC920-FR-01", + "OrderItemId": "12345678901234", + "Title": "Traceur GPS FMC920", + "QuantityOrdered": 2, + "QuantityShipped": 0, + "ItemPrice": { "CurrencyCode": "EUR", "Amount": "99.99" } +} +``` + +## Rate Limits + +| Endpoint | Rate | Burst | +|----------|------|-------| +| `getOrders` | 1 req / 60 sec | 20 | +| `getOrder` | 1 req / 2 sec | 30 | +| `getOrderItems` | 1 req / 2 sec | 30 | + +HTTP 429 en cas de throttling. Header `x-amzn-RateLimit-Limit`. + +## SDK Node.js recommandé + +Package `amazon-sp-api` (le plus simple, gestion auto du throttling + tokens) : + +```bash +pnpm add amazon-sp-api +``` + +```typescript +import { SellingPartner } from 'amazon-sp-api' + +const spClient = new SellingPartner({ + region: 'eu', + refresh_token: '', + credentials: { + SELLING_PARTNER_APP_CLIENT_ID: '', + SELLING_PARTNER_APP_CLIENT_SECRET: '' + }, + options: { + auto_request_throttled: true + } +}) + +const orders = await spClient.callAPI({ + operation: 'getOrders', + endpoint: 'orders', + query: { + MarketplaceIds: ['A13V1IB3VIYZZH'], + CreatedAfter: '2026-02-01T00:00:00Z', + OrderStatuses: ['Unshipped', 'PartiallyShipped'] + } +}) + +const items = await spClient.callAPI({ + operation: 'getOrderItems', + endpoint: 'orders', + path: { orderId: '123-4567890-1234567' } +}) +``` + +## Statuts de commande pertinents + +- `Unshipped` — payée, rien expédié → **à traiter** +- `PartiallyShipped` — partiellement expédié +- `Shipped` — tout expédié +- `Canceled` — annulée + +## Stockage des credentials (notre app) + +Dans la table `api_keys`, même pattern qu'Axonaut : +- `provider=amazon`, `label=client_id` +- `provider=amazon`, `label=client_secret` +- `provider=amazon`, `label=refresh_token` + +## Adresse de livraison + +`GET /orders/v0/orders/{orderId}/address` — nécessite un **Restricted Data Token (RDT)** pour accéder aux PII (données personnelles). +L'adresse est nécessaire pour générer les étiquettes Colissimo. + +## Notes vendeur / commentaires internes + +**IMPORTANT : L'API SP-API ne permet PAS d'ajouter des notes vendeur ou commentaires internes sur une commande.** + +### Ce qui n'existe PAS +- Aucun endpoint pour créer/modifier des notes sur une commande +- Aucun champ `seller_note`, `internal_note` ou `comment` dans l'API Orders v0 ou v2026-01-01 +- Aucun feed POST_ORDER_ACKNOWLEDGEMENT_DATA avec support de notes personnalisées +- Les opérations disponibles se limitent à : `updateShipmentStatus`, `updateVerificationStatus`, `confirmShipment` + +### Alternatives recommandées + +1. **Stockage en base de données locale** (recommandé pour notre projet) + - Créer une table `order_notes` liée à `orders` via `amazon_order_id` + - Permet de stocker commentaires, IMEI scannés, statuts de traitement internes + - Indépendant d'Amazon — aucune synchronisation possible + +2. **Seller Central web interface** (manuel) + - Seller Central propose une interface web pour ajouter des notes sur les commandes MFN + - Ces notes ne sont PAS accessibles via l'API SP-API (lecture ni écriture) + - Utilisation manuelle uniquement + +3. **Champ BuyerCustomizedInfo** (lecture seule) + - Contient des informations de personnalisation **côté acheteur** (texte gravure, etc.) + - Nécessite un Restricted Data Token (RDT) + - Lecture seule — ne peut pas être modifié par le vendeur + +### Conclusion + +Pour tracker les IMEI scannés, les statuts de préparation ou toute donnée interne liée à une commande Amazon, il est nécessaire de gérer ces informations dans notre propre base de données. Amazon SP-API ne fournit aucun mécanisme pour stocker des métadonnées vendeur personnalisées. diff --git a/docs/axonaut-api.md b/docs/axonaut-api.md new file mode 100644 index 0000000..f535498 --- /dev/null +++ b/docs/axonaut-api.md @@ -0,0 +1,361 @@ +# Axonaut API v2 — Documentation de référence + +**Base URL** : `https://axonaut.com/api/v2` +**Doc officielle** : https://axonaut.com/api/v2/doc +**Authentification** : Header `userApiKey` avec la clé API du compte + +--- + +## Contexte Localiztoi + +On n'édite **pas de facture Amazon via Axonaut**. L'intégration sert à : +1. **Destocker** les produits vendus (mise à jour quantité stock) +2. **Créer une observation/note** sur le produit ou la société avec les numéros IMEI + +### Flux prévu +1. Commande Amazon traitée (IMEI scannés + FOTA OK) +2. → `PATCH /products/{id}/stock` pour diminuer la quantité +3. → `POST /events` (nature=6 Note) avec les IMEI dans le champ `content` + +--- + +## Authentification + +Toutes les requêtes nécessitent le header : +``` +userApiKey: +``` + +La clé est stockée dans notre table `api_keys` (provider: `axonaut`, label: `api_key`). + +--- + +## Endpoints pertinents + +### 1. Produits + +#### GET /products +Récupérer tous les produits. + +| Paramètre | Type | Emplacement | Description | +|-----------------|---------|-------------|--------------------------------| +| `page` | integer | header | Pagination | +| `internal_id` | string | query | Filtre par ID interne | +| `product_code` | string | query | Filtre par code produit (SKU) | +| `name` | string | query | Filtre par nom | +| `with_disabled` | boolean | query | Inclure les produits désactivés| + +**Réponse** : Array de produits +```json +[ + { + "id": 123, + "internal_id": "LOC-001", + "product_code": "FMC130", + "name": "Teltonika FMC130", + "description": "Boîtier GPS Teltonika FMC130", + "price": 45.00, + "tax_rate": 20.0, + "type": 707, + "disabled": false + } +] +``` + +**Types de produit** : +- `601` = Matière première +- `701` = Produit fini +- `706` = Service +- `707` = Marchandise ← **notre cas** + +#### GET /products/{productId} +Récupérer un produit spécifique par son ID Axonaut. + +#### POST /products +Créer un produit. + +```json +{ + "name": "Teltonika FMC130", + "product_code": "FMC130", + "price": 45.00, + "tax_rate": 20.0, + "type": 707 +} +``` + +#### PATCH /products/{productId} +Mettre à jour un produit. + +--- + +### 2. Stock + +#### GET /products/{productId}/stock +Récupérer le stock actuel d'un produit. + +**Réponse** : +```json +{ + "product_id": 123, + "quantity": 50, + "reserved": 5 +} +``` +- `quantity` : stock total disponible +- `reserved` : quantité réservée (devis en cours, etc.) + +#### PATCH /products/{productId}/stock ⭐ ENDPOINT CLÉ +Mettre à jour le stock d'un produit (destockage). + +**Body** : +```json +{ + "quantity": 45 +} +``` + +> **IMPORTANT** : C'est une valeur absolue, pas un delta. Pour destocker, il faut : +> 1. `GET /products/{id}/stock` → récupérer `quantity` actuelle +> 2. Calculer `nouvelle_quantité = quantity - nb_articles_vendus` +> 3. `PATCH /products/{id}/stock` avec `{ "quantity": nouvelle_quantité }` + +**Champs disponibles** : +- `quantity` (number) : nouveau stock total +- `reserved` (number) : nouveau stock réservé + +> **Limitation** : Pas de champ commentaire/raison pour le mouvement de stock. +> C'est pour ça qu'on crée une observation/note séparée avec les IMEI. + +--- + +### 3. Événements / Notes (Observations) + +#### POST /events ⭐ ENDPOINT CLÉ +Créer un événement (note, appel, email, etc.) attaché à une société. + +**Body** : +```json +{ + "company_id": 33, + "title": "Destockage Amazon - Commande #AMZ-12345", + "content": "Destockage de 3x FMC130.\n\nIMEI :\n- 352656100123456\n- 352656100123457\n- 352656100123458", + "nature": 6, + "date": "2024-01-15T10:30:00+01:00", + "is_done": true +} +``` + +**Codes nature** : +| Code | Type | +|------|-------------| +| 1 | Rendez-vous | +| 2 | Email | +| 3 | Appel | +| 4 | Courrier | +| 5 | SMS | +| **6**| **Note** ← notre cas | + +**Champs complets** : +| Champ | Type | Description | +|------------------|---------|---------------------------------------------------| +| `company_id` | integer | ID de la société Axonaut (requis sauf si employee) | +| `employee_email` | string | Email du contact (alternatif à company_id) | +| `title` | string | Titre de l'événement | +| `content` | string | Contenu texte libre — **on y met les IMEI** | +| `nature` | integer | Type d'événement (1-6) | +| `date` | string | Date RFC3339 | +| `duration` | integer | Durée en minutes | +| `is_done` | boolean | Marquer comme terminé | +| `opportunity_id` | integer | Lier à une opportunité | +| `users` | array | Liste d'utilisateurs associés | +| `attachments` | object | `invoices_ids`, `quotations_ids`, `documents_ids` | + +#### GET /events +Lister les événements avec filtrage par date. + +| Paramètre | Type | Description | +|-------------------|--------|--------------------------| +| `date[inf]` | string | Date minimum | +| `date[sup]` | string | Date maximum | +| `date[eq]` | string | Date exacte | +| `orders[date]` | string | Tri par date | + +#### GET /companies/{companyId}/events +Lister les événements d'une société spécifique. + +--- + +### 4. Sociétés + +#### GET /companies +Lister les sociétés. + +| Paramètre | Type | Description | +|-------------------|---------|--------------------------------------| +| `search` | string | Recherche par nom | +| `type` | string | `customer`, `prospect`, `supplier`, `all` | +| `is_customer` | boolean | Filtre clients | +| `is_supplier` | boolean | Filtre fournisseurs | +| `siret` | string | Filtre par SIRET | +| `address_city` | string | Filtre par ville | +| `sort` | string | `id`, `name`, `address_city` | + +#### GET /companies/{companyId} +Récupérer une société par ID. + +#### POST /companies +Créer une société. + +```json +{ + "name": "Amazon EU SARL", + "is_customer": true, + "currency": "EUR" +} +``` + +> Pour les B2C : ajouter `"isB2C": true` + données employé + +#### PATCH /companies/{companyId} +Mettre à jour une société. + +#### DELETE /companies/{companyId} +Supprimer une société. Réponse : `202 Accepted`. + +--- + +### 5. Factures (référence, pas utilisé pour Amazon) + +#### GET /invoices +Lister les factures. + +| Paramètre | Type | Description | +|--------------------------|---------|---------------------------| +| `number` | string | Numéro de facture | +| `date_before` / `after` | string | Filtre par date | +| `is_paid` | boolean | Filtre payé/non payé | + +#### POST /invoices +Créer une facture. + +```json +{ + "company_id": 33, + "date": "2024-01-15T00:00:00+01:00", + "title": "Facture FMC130", + "products": [ + { + "product_id": 123, + "quantity": 3, + "price": 45.00 + } + ] +} +``` + +> `company_id` OU `employee_email` requis. +> Types d'acompte : `1` = premier, `2` = acompte, `3` = solde + +#### GET /invoices/{invoiceId} +Récupérer une facture. + +--- + +### 6. Bons de livraison + +#### GET /delivery-forms +Lister les bons de livraison. + +#### POST /delivery-forms +Créer un bon de livraison à partir d'une facture. + +```json +{ + "invoice_id": 456 +} +``` + +#### GET /delivery-forms/{id}/download +Télécharger le PDF. Ajouter `?alt=media` pour le binaire brut. + +--- + +### 7. Paiements + +#### GET /payments +Lister les paiements de factures. + +#### POST /payments +Créer un paiement. + +**Codes nature** : +| Code | Mode | +|------|----------------| +| 1 | Prélèvement | +| 2 | Virement | +| 3 | Chèque | +| 4 | Carte bancaire | +| 5 | Espèces | +| 6 | Autre | + +--- + +## Endpoints secondaires (non prioritaires) + +| Endpoint | Méthode | Description | +|---------------------------|---------|--------------------------------| +| `/employees` | GET/POST| Contacts/employés | +| `/quotations` | GET/POST| Devis | +| `/contracts` | GET/POST| Commandes/contrats récurrents | +| `/expenses` | GET/POST| Dépenses | +| `/opportunities` | CRUD | Pipeline commercial | +| `/tasks` | CRUD | Tâches | +| `/projects` | CRUD | Projets | +| `/tickets` | CRUD | Support/tickets | +| `/suppliers` | CRUD | Fournisseurs | +| `/supplier-contracts` | GET/POST| Commandes fournisseur | +| `/supplier-deliveries` | GET/POST| Livraisons fournisseur | +| `/bank-accounts` | GET | Comptes bancaires | +| `/bank-transactions` | GET | Transactions bancaires | +| `/tax-rates` | GET | Taux de TVA | +| `/accounting-codes` | GET/POST| Codes comptables | +| `/diverse-operations` | GET | Opérations diverses | +| `/workforces` | GET | Effectifs | +| `/payslips` | GET | Bulletins de paie | +| `/me` | GET | Infos compte connecté | +| `/customfields` | GET | Champs personnalisés | +| `/users` | GET | Utilisateurs du compte | +| `/themes` | GET | Thèmes | +| `/languages` | GET | Langues | + +--- + +## Format des dates + +- **RFC3339** pour la plupart des endpoints : `2024-01-15T10:30:00+01:00` +- **DD/MM/YYYY** pour certains filtres spécifiques (credits-history) + +--- + +## Implémentation prévue dans Localiztoi + +### Étape 1 : Mapping produits Axonaut ↔ SKU +- Rechercher les produits Axonaut via `GET /products?product_code=FMC130` +- Stocker le `product_id` Axonaut dans notre table `sku_mappings` + +### Étape 2 : Destockage automatique +Quand une commande est finalisée (tous les IMEI scannés + FOTA OK) : +``` +1. Pour chaque SKU de la commande : + a. GET /products/{axonaut_id}/stock → récupérer quantité actuelle + b. PATCH /products/{axonaut_id}/stock → { quantity: actuelle - nb_vendus } + +2. POST /events (nature=6) sur la société "Amazon" : + - title: "Destockage - Commande Amazon #XXX" + - content: Liste des IMEI par produit +``` + +### Étape 3 : Vérification +- `GET /products/{id}/stock` pour confirmer le nouveau niveau de stock +- Log du mouvement dans notre base (traçabilité) diff --git a/docs/colissimo-api.md b/docs/colissimo-api.md new file mode 100644 index 0000000..077eb65 --- /dev/null +++ b/docs/colissimo-api.md @@ -0,0 +1,141 @@ +# Colissimo Web Service Affranchissement — Documentation de référence + +## Authentification + +Deux modes possibles : + +### Mode 1 : contractNumber + password (dans le body JSON) +```json +{ + "contractNumber": "123456", + "password": "monMotDePasse", + ... +} +``` + +### Mode 2 : API Key (dans le header) +``` +apikey: MA_CLE_API +``` + +## Endpoints + +| Environnement | URL | +|--------------|-----| +| **Production** | `https://ws.colissimo.fr/sls-ws/SlsServiceWSRest/3.1/generateLabel` | +| **Sandbox** | `https://ws.colissimo.fr/sls-ws/SlsServiceWSRest/3.1/checkGenerateLabel` | + +`checkGenerateLabel` = même requête mais sans générer de vrai colis (pas de suivi, pas de facturation). + +## Générer une étiquette (generateLabel) + +**POST** `https://ws.colissimo.fr/sls-ws/SlsServiceWSRest/3.1/generateLabel` + +```json +{ + "contractNumber": "123456", + "password": "monMotDePasse", + "outputFormat": { + "x": 0, + "y": 0, + "outputPrintingType": "PDF_10x15_300dpi" + }, + "letter": { + "service": { + "productCode": "DOM", + "depositDate": "2026-02-20", + "orderNumber": "123-4567890-1234567", + "commercialName": "Localiztoi" + }, + "parcel": { + "weight": 0.5 + }, + "sender": { + "address": { + "companyName": "Localiztoi", + "line2": "Adresse de l'entrepôt", + "countryCode": "FR", + "city": "Ville", + "zipCode": "00000" + } + }, + "addressee": { + "address": { + "lastName": "Dupont", + "firstName": "Jean", + "line2": "10 avenue des Champs", + "countryCode": "FR", + "city": "Lyon", + "zipCode": "69001" + } + } + } +} +``` + +## Champs obligatoires + +| Bloc | Champs | +|------|--------| +| Racine | `contractNumber` + `password` | +| outputFormat | `outputPrintingType` | +| service | `productCode`, `depositDate` | +| parcel | `weight` (max 30 kg) | +| sender.address | `countryCode`, `zipCode`, `city` | +| addressee.address | `lastName`, `line2`, `countryCode`, `city`, `zipCode` | + +## Formats d'étiquette + +| Format | Description | +|--------|------------| +| `PDF_A4_300dpi` | PDF pleine page A4 | +| `PDF_10x15_300dpi` | PDF étiquette 10x15 cm | +| `ZPL_10x15_300dpi` | ZPL (Zebra) 10x15 | + +## Réponse + +La réponse est **multipart MIME (MTOM)** contenant : +1. Une partie JSON avec le statut + numéro de suivi +2. Une partie binaire avec le PDF de l'étiquette + +```json +{ + "messages": [{ "id": "0", "type": "INFOS" }], + "labelResponse": { + "parcelNumber": "6A12345678901", + "parcelNumberPartner": "" + } +} +``` + +## Codes produits + +| Code | Service | +|------|---------| +| **DOM** | Colissimo Domicile sans signature | +| **DOS** | Colissimo Domicile avec signature | +| **HD** | Hors Domicile (point relais) | +| **CECO** | Colissimo Eco France | +| **COM** | Colissimo International | + +## Stockage credentials (notre app) + +Dans la table `api_keys` : +- `provider=laposte`, `label=contract_number` +- `provider=laposte`, `label=password` + +## Parsing MTOM + +La réponse multipart contient : +- Le JSON entre les délimiteurs MIME +- Le PDF délimité par `%PDF` et `%%EOF` + +Il faut parser les parties MIME pour extraire les deux. + +## Méthodes utiles + +| Méthode | Description | +|---------|------------| +| `checkGenerateLabel` | Test sans générer de vrai colis | +| `getLabel` | Ré-imprimer une étiquette existante | +| `generateBordereauByParcelsNumbers` | Bordereau de dépôt | diff --git a/docs/teltonika-fota-api.md b/docs/teltonika-fota-api.md new file mode 100644 index 0000000..b58ead9 --- /dev/null +++ b/docs/teltonika-fota-api.md @@ -0,0 +1,133 @@ +# Teltonika FOTA API (consultation) + +## Objectif +Cette page sert de documentation interne pour consulter l’API Teltonika FOTA afin de : +- récupérer les informations d’un appareil à partir d’un IMEI (lookup) +- mettre à jour l’appareil (ex: affectation à un groupe / une organisation côté FOTA) +- comprendre les contraintes (auth, throttling, headers requis) + +Source : Swagger officiel Teltonika +- UI: https://api.teltonika.lt/documents/index.html +- OpenAPI JSON: https://api.teltonika.lt/swagger/v1/swagger.json + +## Base URL +- `https://api.teltonika.lt` + +## Authentification +L’API utilise un **Bearer token** dans le header `Authorization`. + +Headers minimum : +- `Authorization: Bearer ` +- `User-Agent: //` (requis) +- `Accept: application/json` +- `Content-Type: application/json` (pour les requêtes JSON) + +Notes : +- Le token doit être stocké côté serveur (API), via une variable d’environnement. +- Ne jamais exposer ce token dans le front. + +## Limites / throttling +- Teltonika indique une limite indicative de **~100 requêtes / minute**. +- Pour des actions de masse, privilégier les endpoints `bulk*` quand disponibles. + +## Formats date/heure +- Les champs date/heure sont en **UTC** et formatés `yyyy-MM-dd HH:mm:ss`. + +## Endpoints utiles (lookup + move) + +### 1) Lister des appareils (filtrage par IMEI) +`GET /devices` + +Paramètres de requête utiles : +- `imei` (array[int64]) : filtrer par IMEI +- `query` (string) : recherche plein texte +- `query_field` (string) : champ de recherche (`imei`, `serial`, `description`, `iccid`) +- `group_id` (array[int64]) : filtrer par groupe +- `company_id` (array[int64]) : filtrer par organisation/compagnie + +Exemple (lookup par IMEI) : +```bash +curl -sS 'https://api.teltonika.lt/devices?imei=352625691450983' \ + -H 'Authorization: Bearer ' \ + -H 'User-Agent: localiztoi/stock-app/0.1' \ + -H 'Accept: application/json' +``` + +Réponse : un résultat paginé (schema `DevicePagedResult`). + +### 2) Récupérer un appareil (détails) par IMEI +`GET /devices/{deviceImei}` + +Exemple : +```bash +curl -sS 'https://api.teltonika.lt/devices/352625691450983' \ + -H 'Authorization: Bearer ' \ + -H 'User-Agent: localiztoi/stock-app/0.1' \ + -H 'Accept: application/json' +``` + +Champs intéressants (selon swagger) : +- `imei` (int64) +- `serial` (string) +- `model` (string) +- `current_firmware` (string) +- `activity_status` (int32) +- `company_id` (int64) + `company_name` +- `group_id` (int64) + `group_name` +- `seen_at` (datetime string UTC) +- `iccid`, `imsi` (si présent) + +### 3) Mettre à jour un appareil (affectation groupe / company) +`PUT /devices/{deviceImei}` ou `PATCH /devices/{deviceImei}` + +Le swagger indique que les champs suivants peuvent être modifiés : +- `description` (string) optionnel +- `company_id` (int64) optionnel +- `group_id` (int64) optionnel + +Exemple : affecter un appareil à un groupe : +```bash +curl -sS -X PATCH 'https://api.teltonika.lt/devices/352625691450983' \ + -H 'Authorization: Bearer ' \ + -H 'User-Agent: localiztoi/stock-app/0.1' \ + -H 'Accept: application/json' \ + -H 'Content-Type: application/json' \ + --data '{"group_id": 12345}' +``` + +> Remarque: selon votre configuration FOTA, le “move” vers « vente amazon » peut correspondre à un `group_id` (et parfois aussi `company_id`). + +### 4) Groupes (si besoin) +- `GET /groups` : lister les groupes +- `GET /groups/{groupId}` : détail + +#### Déplacer un groupe vers une autre company +`POST /groups/{groupId}/move` + +Body : +- `target_company_id` (int64) + +Réponse : action asynchrone avec `background_action_id` (suivable via `GET /backgroundActions/{backgroundActionId}`) + +### 5) Background actions (suivi des jobs asynchrones) +`GET /backgroundActions/{backgroundActionId}` + +Utile quand un endpoint renvoie un `background_action_id`. + +## Mapping recommandé pour notre app +Pour un scan IMEI : +1. insérer l’IMEI dans la commande +2. appeler `GET /devices/{imei}` (ou `GET /devices?imei=...`) +3. stocker dans notre DB : `model`, `serial`, `current_firmware`, `activity_status`, `seen_at`, `company_id/name`, `group_id/name` +4. déplacer le device dans le bon groupe : `PATCH /devices/{imei}` avec `group_id` (et éventuellement `company_id`) + +## Sécurité / bonnes pratiques +- Le token doit être dans une variable d’environnement côté API (ex: `TELTONIKA_FOTA_API_TOKEN`). +- Ne pas logger le token. +- Ajouter un `User-Agent` stable (obligatoire). +- Prévoir retries + backoff et gestion 429. + +## Prochaine étape (à valider) +Pour implémenter le move « vente de boitier → vente amazon », il faut confirmer : +- l’ID de la company (organization) cible (`company_id`) +- l’ID du groupe cible (`group_id`) diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs new file mode 100644 index 0000000..447b942 --- /dev/null +++ b/ecosystem.config.cjs @@ -0,0 +1,35 @@ +module.exports = { + apps: [ + { + name: 'localiztoi-api', + cwd: './apps/api', + script: 'dist/index.js', + interpreter: 'node', + interpreter_args: '--env-file=.env', + exec_mode: 'fork', + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: '512M', + env: { + NODE_ENV: 'production', + PORT: 4000 + } + }, + { + name: 'localiztoi-web', + cwd: './apps/web', + script: 'node_modules/next/dist/bin/next', + args: 'start -p 3100', + exec_mode: 'fork', + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: '512M', + env: { + NODE_ENV: 'production', + PORT: 3100 + } + } + ] +} diff --git a/nginx.conf.example b/nginx.conf.example new file mode 100644 index 0000000..93ac9d6 --- /dev/null +++ b/nginx.conf.example @@ -0,0 +1,38 @@ +# Localiztoi Stock — Configuration Nginx +# Copier dans /etc/nginx/sites-available/stock.geolock.fr +# puis ln -s /etc/nginx/sites-available/stock.geolock.fr /etc/nginx/sites-enabled/ + +server { + listen 80; + server_name stock.geolock.fr; + + # API Express (port 4000) + location /api/ { + proxy_pass http://127.0.0.1:4000/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + + # SSE : désactiver le buffering pour les événements temps réel + proxy_buffering off; + proxy_read_timeout 86400s; + } + + # Frontend Next.js (port 3100) + location / { + proxy_pass http://127.0.0.1:3100; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..db1758f --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "gestion-stock-localiztoi", + "private": true, + "packageManager": "pnpm@10.14.0", + "scripts": { + "dev": "pnpm -r dev", + "build": "pnpm -r build", + "lint": "pnpm -r lint" + } +} diff --git a/packages/shared/dist/apiKey.d.ts b/packages/shared/dist/apiKey.d.ts new file mode 100644 index 0000000..5761408 --- /dev/null +++ b/packages/shared/dist/apiKey.d.ts @@ -0,0 +1,20 @@ +import { z } from 'zod'; +export declare const apiProviders: readonly ["amazon", "axonaut", "fota_teltonika", "1nce", "laposte"]; +export type ApiProvider = (typeof apiProviders)[number]; +export declare const apiProviderLabels: Record; +export declare const apiKeyCreateSchema: z.ZodObject<{ + provider: z.ZodEnum<{ + amazon: "amazon"; + axonaut: "axonaut"; + fota_teltonika: "fota_teltonika"; + "1nce": "1nce"; + laposte: "laposte"; + }>; + label: z.ZodString; + value: z.ZodString; +}, z.core.$strip>; +export type ApiKeyCreate = z.infer; +export declare const apiKeyUpdateSchema: z.ZodObject<{ + value: z.ZodString; +}, z.core.$strip>; +export type ApiKeyUpdate = z.infer; diff --git a/packages/shared/dist/apiKey.js b/packages/shared/dist/apiKey.js new file mode 100644 index 0000000..a43bebd --- /dev/null +++ b/packages/shared/dist/apiKey.js @@ -0,0 +1,23 @@ +import { z } from 'zod'; +export const apiProviders = [ + 'amazon', + 'axonaut', + 'fota_teltonika', + '1nce', + 'laposte', +]; +export const apiProviderLabels = { + amazon: 'Amazon SP-API', + axonaut: 'Axonaut', + fota_teltonika: 'FOTA Teltonika', + '1nce': '1NCE (Cartes SIM)', + laposte: 'La Poste (Colissimo)', +}; +export const apiKeyCreateSchema = z.object({ + provider: z.enum(apiProviders), + label: z.string().min(1), + value: z.string().min(1), +}); +export const apiKeyUpdateSchema = z.object({ + value: z.string().min(1), +}); diff --git a/packages/shared/dist/auth.d.ts b/packages/shared/dist/auth.d.ts new file mode 100644 index 0000000..629adde --- /dev/null +++ b/packages/shared/dist/auth.d.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; +export declare const loginSchema: z.ZodObject<{ + username: z.ZodString; + password: z.ZodString; +}, z.core.$strip>; +export declare const registerSchema: z.ZodObject<{ + username: z.ZodString; + password: z.ZodString; + displayName: z.ZodString; +}, z.core.$strip>; +export declare const changePasswordSchema: z.ZodObject<{ + password: z.ZodString; +}, z.core.$strip>; +export type LoginInput = z.infer; +export type RegisterInput = z.infer; +export type ChangePasswordInput = z.infer; +export type AuthUser = { + id: string; + username: string; + displayName: string; +}; diff --git a/packages/shared/dist/auth.js b/packages/shared/dist/auth.js new file mode 100644 index 0000000..5559655 --- /dev/null +++ b/packages/shared/dist/auth.js @@ -0,0 +1,13 @@ +import { z } from 'zod'; +export const loginSchema = z.object({ + username: z.string().min(1, 'Nom d\'utilisateur requis'), + password: z.string().min(1, 'Mot de passe requis'), +}); +export const registerSchema = z.object({ + username: z.string().min(3, 'Minimum 3 caractères').max(30, 'Maximum 30 caractères'), + password: z.string().min(8, 'Minimum 8 caractères'), + displayName: z.string().min(1, 'Nom d\'affichage requis'), +}); +export const changePasswordSchema = z.object({ + password: z.string().min(8, 'Minimum 8 caractères'), +}); diff --git a/packages/shared/dist/imei.d.ts b/packages/shared/dist/imei.d.ts new file mode 100644 index 0000000..61e3b8e --- /dev/null +++ b/packages/shared/dist/imei.d.ts @@ -0,0 +1,5 @@ +import { z } from 'zod'; +export declare const scanImeiSchema: z.ZodObject<{ + imei: z.ZodString; +}, z.core.$strip>; +export type ScanImei = z.infer; diff --git a/packages/shared/dist/imei.js b/packages/shared/dist/imei.js new file mode 100644 index 0000000..cd677cb --- /dev/null +++ b/packages/shared/dist/imei.js @@ -0,0 +1,4 @@ +import { z } from 'zod'; +export const scanImeiSchema = z.object({ + imei: z.string().min(5) +}); diff --git a/packages/shared/dist/index.d.ts b/packages/shared/dist/index.d.ts new file mode 100644 index 0000000..1c007ff --- /dev/null +++ b/packages/shared/dist/index.d.ts @@ -0,0 +1,5 @@ +export * from './skuMapping.js'; +export * from './order.js'; +export * from './imei.js'; +export * from './apiKey.js'; +export * from './auth.js'; diff --git a/packages/shared/dist/index.js b/packages/shared/dist/index.js new file mode 100644 index 0000000..1c007ff --- /dev/null +++ b/packages/shared/dist/index.js @@ -0,0 +1,5 @@ +export * from './skuMapping.js'; +export * from './order.js'; +export * from './imei.js'; +export * from './apiKey.js'; +export * from './auth.js'; diff --git a/packages/shared/dist/order.d.ts b/packages/shared/dist/order.d.ts new file mode 100644 index 0000000..9b2abd1 --- /dev/null +++ b/packages/shared/dist/order.d.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; +export declare const orderItemCreateSchema: z.ZodObject<{ + amazonSku: z.ZodString; + quantity: z.ZodNumber; + title: z.ZodOptional; +}, z.core.$strip>; +export type OrderItemCreate = z.infer; +export declare const orderCreateSchema: z.ZodObject<{ + orderRef: z.ZodString; + items: z.ZodArray; + }, z.core.$strip>>; +}, z.core.$strip>; +export type OrderCreate = z.infer; diff --git a/packages/shared/dist/order.js b/packages/shared/dist/order.js new file mode 100644 index 0000000..2aaf3c6 --- /dev/null +++ b/packages/shared/dist/order.js @@ -0,0 +1,10 @@ +import { z } from 'zod'; +export const orderItemCreateSchema = z.object({ + amazonSku: z.string().min(1), + quantity: z.number().int().positive(), + title: z.string().optional() +}); +export const orderCreateSchema = z.object({ + orderRef: z.string().min(1), + items: z.array(orderItemCreateSchema).min(1) +}); diff --git a/packages/shared/dist/skuMapping.d.ts b/packages/shared/dist/skuMapping.d.ts new file mode 100644 index 0000000..f88776c --- /dev/null +++ b/packages/shared/dist/skuMapping.d.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; +export declare const skuMappingSchema: z.ZodObject<{ + amazonSku: z.ZodString; + enterpriseSku: z.ZodString; + expectedFotaModel: z.ZodString; + axonautProductInternalId: z.ZodString; +}, z.core.$strip>; +export type SkuMapping = z.infer; diff --git a/packages/shared/dist/skuMapping.js b/packages/shared/dist/skuMapping.js new file mode 100644 index 0000000..ad68b3c --- /dev/null +++ b/packages/shared/dist/skuMapping.js @@ -0,0 +1,7 @@ +import { z } from 'zod'; +export const skuMappingSchema = z.object({ + amazonSku: z.string().min(1), + enterpriseSku: z.string().min(1), + expectedFotaModel: z.string().min(1), + axonautProductInternalId: z.string().min(1) +}); diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000..d29104b --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,24 @@ +{ + "name": "@localiztoi/shared", + "private": true, + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "scripts": { + "dev": "tsc -p tsconfig.json --watch", + "build": "tsc -p tsconfig.json", + "lint": "echo \"lint not configured yet\"" + }, + "dependencies": { + "zod": "^4.3.6" + }, + "devDependencies": { + "typescript": "^5.9.3" + } +} diff --git a/packages/shared/src/apiKey.ts b/packages/shared/src/apiKey.ts new file mode 100644 index 0000000..89e4d75 --- /dev/null +++ b/packages/shared/src/apiKey.ts @@ -0,0 +1,33 @@ +import { z } from 'zod' + +export const apiProviders = [ + 'amazon', + 'axonaut', + 'fota_teltonika', + '1nce', + 'laposte', +] as const + +export type ApiProvider = (typeof apiProviders)[number] + +export const apiProviderLabels: Record = { + amazon: 'Amazon SP-API', + axonaut: 'Axonaut', + fota_teltonika: 'FOTA Teltonika', + '1nce': '1NCE (Cartes SIM)', + laposte: 'La Poste (Colissimo)', +} + +export const apiKeyCreateSchema = z.object({ + provider: z.enum(apiProviders), + label: z.string().min(1), + value: z.string().min(1), +}) + +export type ApiKeyCreate = z.infer + +export const apiKeyUpdateSchema = z.object({ + value: z.string().min(1), +}) + +export type ApiKeyUpdate = z.infer diff --git a/packages/shared/src/auth.ts b/packages/shared/src/auth.ts new file mode 100644 index 0000000..9b3142e --- /dev/null +++ b/packages/shared/src/auth.ts @@ -0,0 +1,26 @@ +import { z } from 'zod' + +export const loginSchema = z.object({ + username: z.string().min(1, 'Nom d\'utilisateur requis'), + password: z.string().min(1, 'Mot de passe requis'), +}) + +export const registerSchema = z.object({ + username: z.string().min(3, 'Minimum 3 caractères').max(30, 'Maximum 30 caractères'), + password: z.string().min(8, 'Minimum 8 caractères'), + displayName: z.string().min(1, 'Nom d\'affichage requis'), +}) + +export const changePasswordSchema = z.object({ + password: z.string().min(8, 'Minimum 8 caractères'), +}) + +export type LoginInput = z.infer +export type RegisterInput = z.infer +export type ChangePasswordInput = z.infer + +export type AuthUser = { + id: string + username: string + displayName: string +} diff --git a/packages/shared/src/imei.ts b/packages/shared/src/imei.ts new file mode 100644 index 0000000..e4d7703 --- /dev/null +++ b/packages/shared/src/imei.ts @@ -0,0 +1,7 @@ +import { z } from 'zod' + +export const scanImeiSchema = z.object({ + imei: z.string().min(5) +}) + +export type ScanImei = z.infer diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 0000000..231eae1 --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1,5 @@ +export * from './skuMapping.js' +export * from './order.js' +export * from './imei.js' +export * from './apiKey.js' +export * from './auth.js' diff --git a/packages/shared/src/order.ts b/packages/shared/src/order.ts new file mode 100644 index 0000000..6a89ee8 --- /dev/null +++ b/packages/shared/src/order.ts @@ -0,0 +1,16 @@ +import { z } from 'zod' + +export const orderItemCreateSchema = z.object({ + amazonSku: z.string().min(1), + quantity: z.number().int().positive(), + title: z.string().optional() +}) + +export type OrderItemCreate = z.infer + +export const orderCreateSchema = z.object({ + orderRef: z.string().min(1), + items: z.array(orderItemCreateSchema).min(1) +}) + +export type OrderCreate = z.infer diff --git a/packages/shared/src/skuMapping.ts b/packages/shared/src/skuMapping.ts new file mode 100644 index 0000000..fc50b98 --- /dev/null +++ b/packages/shared/src/skuMapping.ts @@ -0,0 +1,10 @@ +import { z } from 'zod' + +export const skuMappingSchema = z.object({ + amazonSku: z.string().min(1), + enterpriseSku: z.string().min(1), + expectedFotaModel: z.string().min(1), + axonautProductInternalId: z.string().min(1) +}) + +export type SkuMapping = z.infer diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 0000000..3c348f7 --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true, + "declaration": true, + "emitDeclarationOnly": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..6d185eb --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,9480 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: {} + + apps/api: + dependencies: + '@localiztoi/shared': + specifier: workspace:* + version: link:../../packages/shared + amazon-sp-api: + specifier: ^1.2.0 + version: 1.2.0 + axios: + specifier: ^1.13.5 + version: 1.13.5 + bcryptjs: + specifier: ^3.0.3 + version: 3.0.3 + cookie-parser: + specifier: ^1.4.7 + version: 1.4.7 + cors: + specifier: ^2.8.6 + version: 2.8.6 + drizzle-orm: + specifier: ^0.45.1 + version: 0.45.1(@types/pg@8.16.0)(pg@8.18.0) + express: + specifier: ^5.2.1 + version: 5.2.1 + jsonwebtoken: + specifier: ^9.0.3 + version: 9.0.3 + pg: + specifier: ^8.18.0 + version: 8.18.0 + zod: + specifier: ^4.3.6 + version: 4.3.6 + devDependencies: + '@types/bcryptjs': + specifier: ^3.0.0 + version: 3.0.0 + '@types/cookie-parser': + specifier: ^1.4.10 + version: 1.4.10(@types/express@5.0.6) + '@types/cors': + specifier: ^2.8.19 + version: 2.8.19 + '@types/express': + specifier: ^5.0.6 + version: 5.0.6 + '@types/jsonwebtoken': + specifier: ^9.0.10 + version: 9.0.10 + '@types/node': + specifier: ^20.19.33 + version: 20.19.33 + '@types/pg': + specifier: ^8.16.0 + version: 8.16.0 + dotenv-cli: + specifier: ^11.0.0 + version: 11.0.0 + drizzle-kit: + specifier: ^0.31.9 + version: 0.31.9 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + apps/web: + dependencies: + '@localiztoi/shared': + specifier: workspace:* + version: link:../../packages/shared + '@tailwindcss/postcss': + specifier: ^4.1.18 + version: 4.1.18 + axios: + specifier: ^1.13.5 + version: 1.13.5 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + html5-qrcode: + specifier: ^2.3.8 + version: 2.3.8 + lucide-react: + specifier: ^0.574.0 + version: 0.574.0(react@19.2.3) + next: + specifier: 16.1.6 + version: 16.1.6(@babel/core@7.29.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + postcss: + specifier: ^8.5.6 + version: 8.5.6 + radix-ui: + specifier: ^1.4.3 + version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: + specifier: 19.2.3 + version: 19.2.3 + react-dom: + specifier: 19.2.3 + version: 19.2.3(react@19.2.3) + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + tailwind-merge: + specifier: ^3.4.1 + version: 3.4.1 + tailwindcss: + specifier: ^4.1.18 + version: 4.1.18 + devDependencies: + '@types/node': + specifier: ^20 + version: 20.19.33 + '@types/react': + specifier: ^19 + version: 19.2.14 + '@types/react-dom': + specifier: ^19 + version: 19.2.3(@types/react@19.2.14) + babel-plugin-react-compiler: + specifier: 1.0.0 + version: 1.0.0 + eslint: + specifier: ^9 + version: 9.39.2(jiti@2.6.1) + eslint-config-next: + specifier: 16.1.6 + version: 16.1.6(@typescript-eslint/parser@8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + shadcn: + specifier: ^3.8.5 + version: 3.8.5(@types/node@20.19.33)(typescript@5.9.3) + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 + typescript: + specifier: ^5 + version: 5.9.3 + + apps/worker: + dependencies: + bullmq: + specifier: ^5.69.3 + version: 5.69.3 + devDependencies: + '@types/node': + specifier: ^25.2.3 + version: 25.2.3 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + packages/shared: + dependencies: + zod: + specifier: ^4.3.6 + version: 4.3.6 + devDependencies: + typescript: + specifier: ^5.9.3 + version: 5.9.3 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@antfu/ni@25.0.0': + resolution: {integrity: sha512-9q/yCljni37pkMr4sPrI3G4jqdIk074+iukc5aFJl7kmDCCsiJrbZ6zKxnES1Gwg+i9RcDZwvktl23puGslmvA==} + hasBin: true + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.28.6': + resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.28.6': + resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.28.6': + resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.6': + resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-typescript@7.28.5': + resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@dotenvx/dotenvx@1.52.0': + resolution: {integrity: sha512-CaQcc8JvtzQhUSm9877b6V4Tb7HCotkcyud9X2YwdqtQKwgljkMRwU96fVYKnzN3V0Hj74oP7Es+vZ0mS+Aa1w==} + hasBin: true + + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + + '@ecies/ciphers@0.2.5': + resolution: {integrity: sha512-GalEZH4JgOMHYYcYmVqnFirFsjZHeoGMDt9IxEnM9F7GRUUyUksJ7Ou53L83WHJq3RWKD3AcBpo0iQh0oMpf8A==} + engines: {bun: '>=1', deno: '>=2', node: '>=16'} + peerDependencies: + '@noble/ciphers': ^1.0.0 + + '@emnapi/core@1.8.1': + resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.2': + resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.7.4': + resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} + + '@floating-ui/dom@1.7.5': + resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==} + + '@floating-ui/react-dom@2.1.7': + resolution: {integrity: sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@hono/node-server@1.19.9': + resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@ioredis/commands@1.5.0': + resolution: {integrity: sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==} + + '@isaacs/cliui@9.0.0': + resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} + engines: {node: '>=18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@modelcontextprotocol/sdk@1.26.0': + resolution: {integrity: sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} + cpu: [arm64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} + cpu: [x64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} + cpu: [arm64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} + cpu: [arm] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} + cpu: [x64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} + cpu: [x64] + os: [win32] + + '@mswjs/interceptors@0.41.3': + resolution: {integrity: sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==} + engines: {node: '>=18'} + + '@napi-rs/wasm-runtime@0.2.12': + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + + '@next/env@16.1.6': + resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==} + + '@next/eslint-plugin-next@16.1.6': + resolution: {integrity: sha512-/Qq3PTagA6+nYVfryAtQ7/9FEr/6YVyvOtl6rZnGsbReGLf0jZU6gkpr1FuChAQpvV46a78p4cmHOVP8mbfSMQ==} + + '@next/swc-darwin-arm64@16.1.6': + resolution: {integrity: sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@16.1.6': + resolution: {integrity: sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@16.1.6': + resolution: {integrity: sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@16.1.6': + resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@16.1.6': + resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@16.1.6': + resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@16.1.6': + resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@16.1.6': + resolution: {integrity: sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@noble/ciphers@1.3.0': + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.7': + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-accessible-icon@1.1.7': + resolution: {integrity: sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-accordion@1.2.12': + resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-alert-dialog@1.1.15': + resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-aspect-ratio@1.1.7': + resolution: {integrity: sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-avatar@1.1.10': + resolution: {integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collapsible@1.1.12': + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context-menu@2.2.16': + resolution: {integrity: sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.16': + resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-form@0.1.8': + resolution: {integrity: sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-hover-card@1.1.15': + resolution: {integrity: sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.7': + resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.16': + resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menubar@1.1.16': + resolution: {integrity: sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-navigation-menu@1.2.14': + resolution: {integrity: sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-one-time-password-field@0.1.8': + resolution: {integrity: sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-password-toggle-field@0.1.3': + resolution: {integrity: sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-progress@1.1.7': + resolution: {integrity: sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-radio-group@1.3.8': + resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.10': + resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.7': + resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slider@1.3.6': + resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toast@1.2.15': + resolution: {integrity: sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle-group@1.1.11': + resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle@1.1.10': + resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toolbar@1.1.11': + resolution: {integrity: sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-is-hydrated@0.1.0': + resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tailwindcss/node@4.1.18': + resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} + + '@tailwindcss/oxide-android-arm64@4.1.18': + resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.18': + resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.18': + resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} + engines: {node: '>= 10'} + + '@tailwindcss/postcss@4.1.18': + resolution: {integrity: sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==} + + '@ts-morph/common@0.27.0': + resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/bcryptjs@3.0.0': + resolution: {integrity: sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==} + deprecated: This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed. + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cookie-parser@1.4.10': + resolution: {integrity: sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==} + peerDependencies: + '@types/express': '*' + + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@20.19.33': + resolution: {integrity: sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==} + + '@types/node@25.2.3': + resolution: {integrity: sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==} + + '@types/pg@8.16.0': + resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==} + + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + + '@types/statuses@2.0.6': + resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + + '@types/validate-npm-package-name@4.0.2': + resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} + + '@typescript-eslint/eslint-plugin@8.56.0': + resolution: {integrity: sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.56.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.56.0': + resolution: {integrity: sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.56.0': + resolution: {integrity: sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.56.0': + resolution: {integrity: sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.56.0': + resolution: {integrity: sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.56.0': + resolution: {integrity: sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.56.0': + resolution: {integrity: sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.56.0': + resolution: {integrity: sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.56.0': + resolution: {integrity: sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.56.0': + resolution: {integrity: sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.11.1': + resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.11.1': + resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + cpu: [x64] + os: [win32] + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + amazon-sp-api@1.2.0: + resolution: {integrity: sha512-oWm9fvs4psXqkaNcJX5vAvw6oQFGkHHX6lyDJJHqX1PRnF4kN8LyQu97laBRsnjQTnL/FUCDLZ+GYAuZTl/gNQ==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + + ast-types@0.16.1: + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axe-core@4.11.1: + resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} + engines: {node: '>=4'} + + axios@1.13.5: + resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + babel-plugin-react-compiler@1.0.0: + resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.2: + resolution: {integrity: sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==} + engines: {node: 20 || >=22} + + baseline-browser-mapping@2.9.19: + resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} + hasBin: true + + bcryptjs@3.0.3: + resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==} + hasBin: true + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + brace-expansion@5.0.2: + resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==} + engines: {node: 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + bullmq@5.69.3: + resolution: {integrity: sha512-P9uLsR7fDvejH/1m6uur6j7U9mqY6nNt+XvhlhStOUe7jdwbZoP/c2oWNtE+8ljOlubw4pRUKymtRqkyvloc4A==} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001770: + resolution: {integrity: sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + + code-block-writer@13.0.3: + resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-parser@1.4.7: + resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==} + engines: {node: '>= 0.8.0'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + csvtojson@2.0.14: + resolution: {integrity: sha512-F7NNvhhDyob7OsuEGRsH0FM1aqLs/WYITyza3l+hTEEmOK9sGPBlYQZwlVG0ezCojXYpE17lhS5qL6BCOZSPyA==} + engines: {node: '>=8.0.0'} + hasBin: true + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + dedent@1.7.1: + resolution: {integrity: sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + diff@8.0.3: + resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} + engines: {node: '>=0.3.1'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dotenv-cli@11.0.0: + resolution: {integrity: sha512-r5pA8idbk7GFWuHEU7trSTflWcdBpQEK+Aw17UrSHjS6CReuhrrPcyC3zcQBPQvhArRHnBo/h6eLH1fkCvNlww==} + hasBin: true + + dotenv-expand@12.0.3: + resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} + engines: {node: '>=12'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dotenv@17.3.1: + resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} + engines: {node: '>=12'} + + drizzle-kit@0.31.9: + resolution: {integrity: sha512-GViD3IgsXn7trFyBUUHyTFBpH/FsHTxYJ66qdbVggxef4UBPHRYxQaRzYLTuekYnk9i5FIEL9pbBIwMqX/Uwrg==} + hasBin: true + + drizzle-orm@0.45.1: + resolution: {integrity: sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1.13' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@upstash/redis': '>=1.34.7' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@upstash/redis': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + eciesjs@0.4.17: + resolution: {integrity: sha512-TOOURki4G7sD1wDCjj7NfLaXZZ49dFOeEb5y39IXpb8p0hRzVvfvzZHOi5JcT+PpyAbi/Y+lxPb8eTag2WYH8w==} + engines: {bun: '>=1', deno: '>=2', node: '>=16'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.5.286: + resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + enhanced-resolve@5.19.0: + resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} + engines: {node: '>=10.13.0'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-abstract@1.24.1: + resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.2.2: + resolution: {integrity: sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-next@16.1.6: + resolution: {integrity: sha512-vKq40io2B0XtkkNDYyleATwblNt8xuh3FWp8SpSz3pt7P01OkBFlKsJZ2mWt5WsCySlDQLckb1zMY9yE9Qy0LA==} + peerDependencies: + eslint: '>=9.0.0' + typescript: '>=3.3.1' + peerDependenciesMeta: + typescript: + optional: true + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-import-resolver-typescript@3.10.1: + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jsx-a11y@6.10.2: + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + + eslint-plugin-react-hooks@7.0.1: + resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.0: + resolution: {integrity: sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.2: + resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + execa@9.6.1: + resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} + engines: {node: ^18.19.0 || >=20.5.0} + + express-rate-limit@8.2.1: + resolution: {integrity: sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fast-xml-parser@5.3.6: + resolution: {integrity: sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA==} + hasBin: true + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-extra@11.3.3: + resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} + engines: {node: '>=14.14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + fuzzysort@3.1.0: + resolution: {integrity: sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==} + + fzf@0.5.2: + resolution: {integrity: sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.4.0: + resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-own-enumerable-keys@1.0.0: + resolution: {integrity: sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==} + engines: {node: '>=14.16'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.13.6: + resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.4.0: + resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphql@16.12.0: + resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + hono@4.11.9: + resolution: {integrity: sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==} + engines: {node: '>=16.9.0'} + + html5-qrcode@2.3.8: + resolution: {integrity: sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + ioredis@5.9.2: + resolution: {integrity: sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==} + engines: {node: '>=12.22.0'} + + ip-address@10.0.1: + resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-obj@3.0.0: + resolution: {integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==} + engines: {node: '>=12'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-regexp@3.1.0: + resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==} + engines: {node: '>=12'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isexe@3.1.5: + resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} + engines: {node: '>=18'} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + jackspeak@4.2.3: + resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} + engines: {node: 20 || >=22} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + + log-symbols@6.0.0: + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + engines: {node: '>=18'} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@0.574.0: + resolution: {integrity: sha512-dJ8xb5juiZVIbdSn3HTyHsjjIwUwZ4FNwV0RtYDScOyySOeie1oXZTymST6YPJ4Qwt3Po8g4quhYl4OxtACiuQ==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} + engines: {node: '>=12'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimatch@10.2.1: + resolution: {integrity: sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==} + engines: {node: 20 || >=22} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + msgpackr-extract@3.0.3: + resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} + hasBin: true + + msgpackr@1.11.5: + resolution: {integrity: sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==} + + msw@2.12.10: + resolution: {integrity: sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + + next@16.1.6: + resolution: {integrity: sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + node-abort-controller@3.1.1: + resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-exports-info@1.6.0: + resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} + engines: {node: '>= 0.4'} + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-gyp-build-optional-packages@5.2.2: + resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} + hasBin: true + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object-treeify@1.1.33: + resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==} + engines: {node: '>= 10'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + open@11.0.0: + resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} + engines: {node: '>=20'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@8.2.0: + resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} + engines: {node: '>=18'} + + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + + pg-cloudflare@1.3.0: + resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} + + pg-connection-string@2.11.0: + resolution: {integrity: sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.11.0: + resolution: {integrity: sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.11.0: + resolution: {integrity: sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.18.0: + resolution: {integrity: sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} + engines: {node: '>=20'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + engines: {node: '>=0.6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + radix-ui@1.4.3: + resolution: {integrity: sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + react-dom@19.2.3: + resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} + peerDependencies: + react: ^19.2.3 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react@19.2.3: + resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} + engines: {node: '>=0.10.0'} + + recast@0.23.11: + resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} + engines: {node: '>= 4'} + + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + resolve@2.0.0-next.6: + resolution: {integrity: sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==} + engines: {node: '>= 0.4'} + hasBin: true + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + rettime@0.10.1: + resolution: {integrity: sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shadcn@3.8.5: + resolution: {integrity: sha512-jPRx44e+eyeV7xwY3BLJXcfrks00+M0h5BGB9l6DdcBW4BpAj4x3lVmVy0TXPEs2iHEisxejr62sZAAw6B1EVA==} + hasBin: true + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + stringify-object@5.0.0: + resolution: {integrity: sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==} + engines: {node: '>=14.16'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strnum@2.1.2: + resolution: {integrity: sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + + tailwind-merge@3.4.1: + resolution: {integrity: sha512-2OA0rFqWOkITEAOFWSBSApYkDeH9t2B3XSJuI4YztKBzK3mX0737A2qtxDZ7xkw9Zfh0bWl+r34sF3HXV+Ig7Q==} + + tailwindcss@4.1.18: + resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tldts-core@7.0.23: + resolution: {integrity: sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==} + + tldts@7.0.23: + resolution: {integrity: sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==} + hasBin: true + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-morph@26.0.0: + resolution: {integrity: sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==} + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@5.4.4: + resolution: {integrity: sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==} + engines: {node: '>=20'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript-eslint@8.56.0: + resolution: {integrity: sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unrs-resolver@1.11.1: + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + + until-async@3.0.2: + resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + validate-npm-package-name@7.0.2: + resolution: {integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==} + engines: {node: ^20.17.0 || >=22.9.0} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + wsl-utils@0.3.1: + resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} + engines: {node: '>=20'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@antfu/ni@25.0.0': + dependencies: + ansis: 4.2.0 + fzf: 0.5.2 + package-manager-detector: 1.6.0 + tinyexec: 1.0.2 + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.29.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.28.5': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/preset-typescript@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@dotenvx/dotenvx@1.52.0': + dependencies: + commander: 11.1.0 + dotenv: 17.3.1 + eciesjs: 0.4.17 + execa: 5.1.1 + fdir: 6.5.0(picomatch@4.0.3) + ignore: 5.3.2 + object-treeify: 1.1.33 + picomatch: 4.0.3 + which: 4.0.0 + + '@drizzle-team/brocli@0.10.2': {} + + '@ecies/ciphers@0.2.5(@noble/ciphers@1.3.0)': + dependencies: + '@noble/ciphers': 1.3.0 + + '@emnapi/core@1.8.1': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.13.6 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': + dependencies: + eslint: 9.39.2(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.3': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.2': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@floating-ui/core@1.7.4': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.5': + dependencies: + '@floating-ui/core': 1.7.4 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/react-dom@2.1.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@floating-ui/dom': 1.7.5 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + '@floating-ui/utils@0.2.10': {} + + '@hono/node-server@1.19.9(hono@4.11.9)': + dependencies: + hono: 4.11.9 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@img/colour@1.0.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.8.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@inquirer/ansi@1.0.2': {} + + '@inquirer/confirm@5.1.21(@types/node@20.19.33)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@20.19.33) + '@inquirer/type': 3.0.10(@types/node@20.19.33) + optionalDependencies: + '@types/node': 20.19.33 + + '@inquirer/core@10.3.2(@types/node@20.19.33)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@20.19.33) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 20.19.33 + + '@inquirer/figures@1.0.15': {} + + '@inquirer/type@3.0.10(@types/node@20.19.33)': + optionalDependencies: + '@types/node': 20.19.33 + + '@ioredis/commands@1.5.0': {} + + '@isaacs/cliui@9.0.0': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@modelcontextprotocol/sdk@1.26.0(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.9(hono@4.11.9) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.2.1(express@5.2.1) + hono: 4.11.9 + jose: 6.1.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + transitivePeerDependencies: + - supports-color + + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + optional: true + + '@mswjs/interceptors@0.41.3': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + + '@napi-rs/wasm-runtime@0.2.12': + dependencies: + '@emnapi/core': 1.8.1 + '@emnapi/runtime': 1.8.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@next/env@16.1.6': {} + + '@next/eslint-plugin-next@16.1.6': + dependencies: + fast-glob: 3.3.1 + + '@next/swc-darwin-arm64@16.1.6': + optional: true + + '@next/swc-darwin-x64@16.1.6': + optional: true + + '@next/swc-linux-arm64-gnu@16.1.6': + optional: true + + '@next/swc-linux-arm64-musl@16.1.6': + optional: true + + '@next/swc-linux-x64-gnu@16.1.6': + optional: true + + '@next/swc-linux-x64-musl@16.1.6': + optional: true + + '@next/swc-win32-arm64-msvc@16.1.6': + optional: true + + '@next/swc-win32-x64-msvc@16.1.6': + optional: true + + '@noble/ciphers@1.3.0': {} + + '@noble/curves@1.9.7': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.8.0': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@nolyfill/is-core-module@1.0.39': {} + + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-accessible-icon@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-aspect-ratio@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-avatar@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.3) + aria-hidden: 1.2.6 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-form@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-hover-card@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-label@2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.3) + aria-hidden: 1.2.6 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-menubar@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-one-time-password-field@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-password-toggle-field@0.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.3) + aria-hidden: 1.2.6 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@floating-ui/react-dom': 2.1.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/rect': 1.1.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-progress@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + aria-hidden: 1.2.6 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-separator@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toast@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toolbar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.14)(react@19.2.3)': + dependencies: + react: 19.2.3 + use-sync-external-store: 1.6.0(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.3)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/rect@1.1.1': {} + + '@rtsao/scc@1.1.0': {} + + '@sec-ant/readable-stream@0.4.1': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.1.18': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.19.0 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.18 + + '@tailwindcss/oxide-android-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide@4.1.18': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-x64': 4.1.18 + '@tailwindcss/oxide-freebsd-x64': 4.1.18 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-x64-musl': 4.1.18 + '@tailwindcss/oxide-wasm32-wasi': 4.1.18 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 + + '@tailwindcss/postcss@4.1.18': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.1.18 + '@tailwindcss/oxide': 4.1.18 + postcss: 8.5.6 + tailwindcss: 4.1.18 + + '@ts-morph/common@0.27.0': + dependencies: + fast-glob: 3.3.3 + minimatch: 10.2.1 + path-browserify: 1.0.1 + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/bcryptjs@3.0.0': + dependencies: + bcryptjs: 3.0.3 + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.19.33 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 20.19.33 + + '@types/cookie-parser@1.4.10(@types/express@5.0.6)': + dependencies: + '@types/express': 5.0.6 + + '@types/cors@2.8.19': + dependencies: + '@types/node': 20.19.33 + + '@types/estree@1.0.8': {} + + '@types/express-serve-static-core@5.1.1': + dependencies: + '@types/node': 20.19.33 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.1 + '@types/serve-static': 2.2.0 + + '@types/http-errors@2.0.5': {} + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 20.19.33 + + '@types/ms@2.1.0': {} + + '@types/node@20.19.33': + dependencies: + undici-types: 6.21.0 + + '@types/node@25.2.3': + dependencies: + undici-types: 7.16.0 + + '@types/pg@8.16.0': + dependencies: + '@types/node': 20.19.33 + pg-protocol: 1.11.0 + pg-types: 2.2.0 + + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@types/send@1.2.1': + dependencies: + '@types/node': 20.19.33 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 20.19.33 + + '@types/statuses@2.0.6': {} + + '@types/validate-npm-package-name@4.0.2': {} + + '@typescript-eslint/eslint-plugin@8.56.0(@typescript-eslint/parser@8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.56.0 + '@typescript-eslint/type-utils': 8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.0 + eslint: 9.39.2(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.56.0 + '@typescript-eslint/types': 8.56.0 + '@typescript-eslint/typescript-estree': 8.56.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.0 + debug: 4.4.3 + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.56.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.56.0(typescript@5.9.3) + '@typescript-eslint/types': 8.56.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.56.0': + dependencies: + '@typescript-eslint/types': 8.56.0 + '@typescript-eslint/visitor-keys': 8.56.0 + + '@typescript-eslint/tsconfig-utils@8.56.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.56.0 + '@typescript-eslint/typescript-estree': 8.56.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.2(jiti@2.6.1) + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.56.0': {} + + '@typescript-eslint/typescript-estree@8.56.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.56.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.56.0(typescript@5.9.3) + '@typescript-eslint/types': 8.56.0 + '@typescript-eslint/visitor-keys': 8.56.0 + debug: 4.4.3 + minimatch: 9.0.5 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.56.0 + '@typescript-eslint/types': 8.56.0 + '@typescript-eslint/typescript-estree': 8.56.0(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.56.0': + dependencies: + '@typescript-eslint/types': 8.56.0 + eslint-visitor-keys: 5.0.0 + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + optional: true + + '@unrs/resolver-binding-android-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + dependencies: + '@napi-rs/wasm-runtime': 0.2.12 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + optional: true + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + agent-base@7.1.4: {} + + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + amazon-sp-api@1.2.0: + dependencies: + csvtojson: 2.0.14 + fast-xml-parser: 5.3.6 + iconv-lite: 0.7.2 + qs: 6.15.0 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansis@4.2.0: {} + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + ast-types-flow@0.0.8: {} + + ast-types@0.16.1: + dependencies: + tslib: 2.8.1 + + async-function@1.0.0: {} + + asynckit@0.4.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axe-core@4.11.1: {} + + axios@1.13.5: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + axobject-query@4.1.0: {} + + babel-plugin-react-compiler@1.0.0: + dependencies: + '@babel/types': 7.29.0 + + balanced-match@1.0.2: {} + + balanced-match@4.0.2: + dependencies: + jackspeak: 4.2.3 + + baseline-browser-mapping@2.9.19: {} + + bcryptjs@3.0.3: {} + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.2: + dependencies: + balanced-match: 4.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.19 + caniuse-lite: 1.0.30001770 + electron-to-chromium: 1.5.286 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + buffer-equal-constant-time@1.0.1: {} + + buffer-from@1.1.2: {} + + bullmq@5.69.3: + dependencies: + cron-parser: 4.9.0 + ioredis: 5.9.2 + msgpackr: 1.11.5 + node-abort-controller: 3.1.1 + semver: 7.7.4 + tslib: 2.8.1 + uuid: 11.1.0 + transitivePeerDependencies: + - supports-color + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001770: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-spinners@2.9.2: {} + + cli-width@4.1.0: {} + + client-only@0.0.1: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clsx@2.1.1: {} + + cluster-key-slot@1.1.2: {} + + code-block-writer@13.0.3: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@11.1.0: {} + + commander@14.0.3: {} + + concat-map@0.0.1: {} + + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + + convert-source-map@2.0.0: {} + + cookie-parser@1.4.7: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.6 + + cookie-signature@1.0.6: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cookie@1.1.1: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cosmiconfig@9.0.0(typescript@5.9.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.9.3 + + cron-parser@4.9.0: + dependencies: + luxon: 3.7.2 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + csvtojson@2.0.14: + dependencies: + lodash: 4.17.23 + + damerau-levenshtein@1.0.8: {} + + data-uri-to-buffer@4.0.1: {} + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + dedent@1.7.1: {} + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-lazy-prop@3.0.0: {} + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + delayed-stream@1.0.0: {} + + denque@2.1.0: {} + + depd@2.0.0: {} + + detect-libc@2.1.2: {} + + detect-node-es@1.1.0: {} + + diff@8.0.3: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dotenv-cli@11.0.0: + dependencies: + cross-spawn: 7.0.6 + dotenv: 17.3.1 + dotenv-expand: 12.0.3 + minimist: 1.2.8 + + dotenv-expand@12.0.3: + dependencies: + dotenv: 16.6.1 + + dotenv@16.6.1: {} + + dotenv@17.3.1: {} + + drizzle-kit@0.31.9: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.25.12 + esbuild-register: 3.6.0(esbuild@0.25.12) + transitivePeerDependencies: + - supports-color + + drizzle-orm@0.45.1(@types/pg@8.16.0)(pg@8.18.0): + optionalDependencies: + '@types/pg': 8.16.0 + pg: 8.18.0 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + eciesjs@0.4.17: + dependencies: + '@ecies/ciphers': 0.2.5(@noble/ciphers@1.3.0) + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.286: {} + + emoji-regex@10.6.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + encodeurl@2.0.0: {} + + enhanced-resolve@5.19.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + env-paths@2.2.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-abstract@1.24.1: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.20 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.2.2: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + safe-array-concat: 1.1.3 + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + esbuild-register@3.6.0(esbuild@0.25.12): + dependencies: + debug: 4.4.3 + esbuild: 0.25.12 + transitivePeerDependencies: + - supports-color + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-next@16.1.6(@typescript-eslint/parser@8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@next/eslint-plugin-next': 16.1.6 + eslint: 9.39.2(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1)) + globals: 16.4.0 + typescript-eslint: 8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3 + eslint: 9.39.2(jiti@2.6.1) + get-tsconfig: 4.13.6 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.15 + unrs-resolver: 1.11.1 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.39.2(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.2(jiti@2.6.1)): + dependencies: + aria-query: 5.3.2 + array-includes: 3.1.9 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.11.1 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 9.39.2(jiti@2.6.1) + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + + eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@2.6.1)): + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.0 + eslint: 9.39.2(jiti@2.6.1) + hermes-parser: 0.25.1 + zod: 4.3.6 + zod-validation-error: 4.0.2(zod@4.3.6) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@2.6.1)): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.2 + eslint: 9.39.2(jiti@2.6.1) + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.6 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.0: {} + + eslint@9.39.2(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.39.2 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esprima@4.0.1: {} + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + etag@1.8.1: {} + + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + execa@9.6.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.2 + + express-rate-limit@8.2.1(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.0.1 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.1: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.1.0: {} + + fast-xml-parser@5.3.6: + dependencies: + strnum: 2.1.2 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + follow-redirects@1.15.11: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fs-extra@11.3.3: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + fuzzysort@3.1.0: {} + + fzf@0.5.2: {} + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.4.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-nonce@1.0.1: {} + + get-own-enumerable-keys@1.0.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@6.0.1: {} + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.13.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@16.4.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphql@16.12.0: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + headers-polyfill@4.0.3: {} + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + hono@4.11.9: {} + + html5-qrcode@2.3.8: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + human-signals@2.1.0: {} + + human-signals@8.0.1: {} + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inherits@2.0.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + ioredis@5.9.2: + dependencies: + '@ioredis/commands': 1.5.0 + cluster-key-slot: 1.1.2 + debug: 4.4.3 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + + ip-address@10.0.1: {} + + ipaddr.js@1.9.1: {} + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.2.1: {} + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-bun-module@2.0.0: + dependencies: + semver: 7.7.4 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-docker@3.0.0: {} + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-in-ssh@1.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-interactive@2.0.0: {} + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-node-process@1.2.0: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-obj@3.0.0: {} + + is-plain-obj@4.1.0: {} + + is-promise@4.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-regexp@3.1.0: {} + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-stream@2.0.1: {} + + is-stream@4.0.1: {} + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.20 + + is-unicode-supported@1.3.0: {} + + is-unicode-supported@2.1.0: {} + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + isexe@3.1.5: {} + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + jackspeak@4.2.3: + dependencies: + '@isaacs/cliui': 9.0.0 + + jiti@2.6.1: {} + + jose@6.1.3: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.4 + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kleur@3.0.3: {} + + kleur@4.1.5: {} + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + lines-and-columns@1.2.4: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.defaults@4.2.0: {} + + lodash.includes@4.3.0: {} + + lodash.isarguments@3.1.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.merge@4.6.2: {} + + lodash.once@4.1.1: {} + + lodash@4.17.23: {} + + log-symbols@6.0.0: + dependencies: + chalk: 5.6.2 + is-unicode-supported: 1.3.0 + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@0.574.0(react@19.2.3): + dependencies: + react: 19.2.3 + + luxon@3.7.2: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mimic-fn@2.1.0: {} + + mimic-function@5.0.1: {} + + minimatch@10.2.1: + dependencies: + brace-expansion: 5.0.2 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + ms@2.1.3: {} + + msgpackr-extract@3.0.3: + dependencies: + node-gyp-build-optional-packages: 5.2.2 + optionalDependencies: + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 + optional: true + + msgpackr@1.11.5: + optionalDependencies: + msgpackr-extract: 3.0.3 + + msw@2.12.10(@types/node@20.19.33)(typescript@5.9.3): + dependencies: + '@inquirer/confirm': 5.1.21(@types/node@20.19.33) + '@mswjs/interceptors': 0.41.3 + '@open-draft/deferred-promise': 2.2.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.12.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.10.1 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.0 + type-fest: 5.4.4 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/node' + + mute-stream@2.0.0: {} + + nanoid@3.3.11: {} + + napi-postinstall@0.3.4: {} + + natural-compare@1.4.0: {} + + negotiator@1.0.0: {} + + next-themes@0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + next@16.1.6(@babel/core@7.29.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@next/env': 16.1.6 + '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.9.19 + caniuse-lite: 1.0.30001770 + postcss: 8.4.31 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.3) + optionalDependencies: + '@next/swc-darwin-arm64': 16.1.6 + '@next/swc-darwin-x64': 16.1.6 + '@next/swc-linux-arm64-gnu': 16.1.6 + '@next/swc-linux-arm64-musl': 16.1.6 + '@next/swc-linux-x64-gnu': 16.1.6 + '@next/swc-linux-x64-musl': 16.1.6 + '@next/swc-win32-arm64-msvc': 16.1.6 + '@next/swc-win32-x64-msvc': 16.1.6 + babel-plugin-react-compiler: 1.0.0 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + node-abort-controller@3.1.1: {} + + node-domexception@1.0.0: {} + + node-exports-info@1.6.0: + dependencies: + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + node-gyp-build-optional-packages@5.2.2: + dependencies: + detect-libc: 2.1.2 + optional: true + + node-releases@2.0.27: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object-treeify@1.1.33: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + open@11.0.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.1.0 + wsl-utils: 0.3.1 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@8.2.0: + dependencies: + chalk: 5.6.2 + cli-cursor: 5.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 6.0.0 + stdin-discarder: 0.2.2 + string-width: 7.2.0 + strip-ansi: 7.1.2 + + outvariant@1.4.3: {} + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + package-manager-detector@1.6.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-ms@4.0.0: {} + + parseurl@1.3.3: {} + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-parse@1.0.7: {} + + path-to-regexp@6.3.0: {} + + path-to-regexp@8.3.0: {} + + pg-cloudflare@1.3.0: + optional: true + + pg-connection-string@2.11.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.11.0(pg@8.18.0): + dependencies: + pg: 8.18.0 + + pg-protocol@1.11.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.18.0: + dependencies: + pg-connection-string: 2.11.0 + pg-pool: 3.11.0(pg@8.18.0) + pg-protocol: 1.11.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.3.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pkce-challenge@5.0.1: {} + + possible-typed-array-names@1.1.0: {} + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + powershell-utils@0.1.0: {} + + prelude-ls@1.2.1: {} + + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-from-env@1.1.0: {} + + punycode@2.3.1: {} + + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + + queue-microtask@1.2.3: {} + + radix-ui@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-accessible-icon': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-alert-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-aspect-ratio': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-avatar': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-checkbox': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-context-menu': 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-form': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-hover-card': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-menubar': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-navigation-menu': 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-one-time-password-field': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-password-toggle-field': 0.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-progress': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-radio-group': 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-scroll-area': 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-select': 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slider': 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-switch': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-toast': 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-toolbar': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + react-dom@19.2.3(react@19.2.3): + dependencies: + react: 19.2.3 + scheduler: 0.27.0 + + react-is@16.13.1: {} + + react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.3): + dependencies: + react: 19.2.3 + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.3) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.3): + dependencies: + react: 19.2.3 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.3) + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.3) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.3) + use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + + react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.3): + dependencies: + get-nonce: 1.0.1 + react: 19.2.3 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + react@19.2.3: {} + + recast@0.23.11: + dependencies: + ast-types: 0.16.1 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@2.0.0-next.6: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.1 + node-exports-info: 1.6.0 + object-keys: 1.1.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + rettime@0.10.1: {} + + reusify@1.1.0: {} + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + + run-applescript@7.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-buffer@5.2.1: {} + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safer-buffer@2.1.2: {} + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + semver@7.7.4: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + setprototypeof@1.2.0: {} + + shadcn@3.8.5(@types/node@20.19.33)(typescript@5.9.3): + dependencies: + '@antfu/ni': 25.0.0 + '@babel/core': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) + '@dotenvx/dotenvx': 1.52.0 + '@modelcontextprotocol/sdk': 1.26.0(zod@3.25.76) + '@types/validate-npm-package-name': 4.0.2 + browserslist: 4.28.1 + commander: 14.0.3 + cosmiconfig: 9.0.0(typescript@5.9.3) + dedent: 1.7.1 + deepmerge: 4.3.1 + diff: 8.0.3 + execa: 9.6.1 + fast-glob: 3.3.3 + fs-extra: 11.3.3 + fuzzysort: 3.1.0 + https-proxy-agent: 7.0.6 + kleur: 4.1.5 + msw: 2.12.10(@types/node@20.19.33)(typescript@5.9.3) + node-fetch: 3.3.2 + open: 11.0.0 + ora: 8.2.0 + postcss: 8.5.6 + postcss-selector-parser: 7.1.1 + prompts: 2.4.2 + recast: 0.23.11 + stringify-object: 5.0.0 + tailwind-merge: 3.4.1 + ts-morph: 26.0.0 + tsconfig-paths: 4.2.0 + validate-npm-package-name: 7.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + transitivePeerDependencies: + - '@cfworker/json-schema' + - '@types/node' + - babel-plugin-macros + - supports-color + - typescript + + sharp@0.34.5: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + sisteransi@1.0.5: {} + + sonner@2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + split2@4.2.0: {} + + stable-hash@0.0.5: {} + + standard-as-callback@2.1.0: {} + + statuses@2.0.2: {} + + stdin-discarder@0.2.2: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + strict-event-emitter@0.5.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.4.0 + strip-ansi: 7.1.2 + + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.1 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + stringify-object@5.0.0: + dependencies: + get-own-enumerable-keys: 1.0.0 + is-obj: 3.0.0 + is-regexp: 3.1.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@3.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-final-newline@4.0.0: {} + + strip-json-comments@3.1.1: {} + + strnum@2.1.2: {} + + styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.3): + dependencies: + client-only: 0.0.1 + react: 19.2.3 + optionalDependencies: + '@babel/core': 7.29.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tagged-tag@1.0.0: {} + + tailwind-merge@3.4.1: {} + + tailwindcss@4.1.18: {} + + tapable@2.3.0: {} + + tiny-invariant@1.3.3: {} + + tinyexec@1.0.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tldts-core@7.0.23: {} + + tldts@7.0.23: + dependencies: + tldts-core: 7.0.23 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.23 + + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-morph@26.0.0: + dependencies: + '@ts-morph/common': 0.27.0 + code-block-writer: 13.0.3 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.3 + get-tsconfig: 4.13.6 + optionalDependencies: + fsevents: 2.3.3 + + tw-animate-css@1.4.0: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@5.4.4: + dependencies: + tagged-tag: 1.0.0 + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript-eslint@8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.56.0(@typescript-eslint/parser@8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.56.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@6.21.0: {} + + undici-types@7.16.0: {} + + unicorn-magic@0.3.0: {} + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + unrs-resolver@1.11.1: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.11.1 + '@unrs/resolver-binding-android-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-x64': 1.11.1 + '@unrs/resolver-binding-freebsd-x64': 1.11.1 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 + '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-musl': 1.11.1 + '@unrs/resolver-binding-wasm32-wasi': 1.11.1 + '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 + '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 + '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + + until-async@3.0.2: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.3): + dependencies: + react: 19.2.3 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.3): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.3 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + use-sync-external-store@1.6.0(react@19.2.3): + dependencies: + react: 19.2.3 + + util-deprecate@1.0.2: {} + + uuid@11.1.0: {} + + validate-npm-package-name@7.0.2: {} + + vary@1.1.2: {} + + web-streams-polyfill@3.3.3: {} + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.20 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + which@4.0.0: + dependencies: + isexe: 3.1.5 + + word-wrap@1.2.5: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + wsl-utils@0.3.1: + dependencies: + is-wsl: 3.1.1 + powershell-utils: 0.1.0 + + xtend@4.0.2: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + yoctocolors-cjs@2.1.3: {} + + yoctocolors@2.1.2: {} + + zod-to-json-schema@3.25.1(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod-validation-error@4.0.2(zod@4.3.6): + dependencies: + zod: 4.3.6 + + zod@3.25.76: {} + + zod@4.3.6: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..3ff5faa --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - "apps/*" + - "packages/*"