chore(lint): manual cleanup batch 4 — noNonNullAssertion + small fixes
- noNonNullAssertion (42 → 0): replaced ! assertions with explicit null guards across auth.module, storage.service, pl24-auth, categories.service, catalog.service, parts.service, emex.browser, emex.service, parts-catalogs-auth, pl24-ford-legacy, email.service, database.provider, jobs/processors, psa-variant-selector, vehicles/$id/categories, catalog/$brandName - buildTree: skip orphaned items instead of asserting map.get - emex.browser.acquirePage: explicit context check before newPage - pcat-auth.getIstanbulTime: graceful UTC fallback if Intl parts missing - email.send: gate on both postalApiUrl + postalApiKey for type narrowing - categories.service: PSA root-fallback now early-returns when category.vehicleId missing (catalog-only categories don't have a sibling root) - pl24-ford-legacy: createHash from "node:crypto", URL building uses single template literals, useDefaultParameterLast — required modelYear/engine/gearbox params (callers already supply them) - pcat-auth: h.origin / h.referer literal-key access - index.tsx: <button type="button"> on mobile menu toggle - parts-panel + admin/users: keyboard handler for clickable rows (Enter/Space) - ford-legacy: useOptionalChain on item.name?.toUpperCase() Lint count: 155 → 114. Remaining: 121 noExplicitAny + 3 small. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,9 +17,14 @@ export class AuthModule implements OnModuleInit {
|
||||
) {}
|
||||
|
||||
onModuleInit() {
|
||||
const databaseUrl = this.configService.get<string>("database.url")!;
|
||||
const secret = this.configService.get<string>("auth.secret")!;
|
||||
const baseUrl = this.configService.get<string>("auth.url")!;
|
||||
const databaseUrl = this.configService.get<string>("database.url");
|
||||
const secret = this.configService.get<string>("auth.secret");
|
||||
const baseUrl = this.configService.get<string>("auth.url");
|
||||
if (!databaseUrl || !secret || !baseUrl) {
|
||||
throw new Error(
|
||||
"Auth module requires database.url, auth.secret, and auth.url to be set",
|
||||
);
|
||||
}
|
||||
const googleClientId = this.configService.get<string>("auth.googleClientId");
|
||||
const googleClientSecret = this.configService.get<string>("auth.googleClientSecret");
|
||||
createAuth(databaseUrl, secret, baseUrl, {
|
||||
|
||||
@@ -944,12 +944,11 @@ export class CatalogService {
|
||||
description: p.description || null,
|
||||
quantity: p.quantity ? Number.parseInt(String(p.quantity), 10) || null : null,
|
||||
position: p.positionCode || null,
|
||||
hotspotIndex: p.hotspotId
|
||||
? (() => {
|
||||
const val = Number.parseInt(p.hotspotId!, 10);
|
||||
return val > 0 && val <= 2147483647 ? val : null;
|
||||
})()
|
||||
: null,
|
||||
hotspotIndex: ((): number | null => {
|
||||
if (!p.hotspotId) return null;
|
||||
const val = Number.parseInt(p.hotspotId, 10);
|
||||
return val > 0 && val <= 2147483647 ? val : null;
|
||||
})(),
|
||||
unavailable: p.unavailable || false,
|
||||
remark: p.remark || null,
|
||||
modelCodes: p.modelCodes || null,
|
||||
@@ -1323,7 +1322,8 @@ export class CatalogService {
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
const node = map.get(item.id)!;
|
||||
const node = map.get(item.id);
|
||||
if (!node) continue;
|
||||
if (item.parentId && map.has(item.parentId)) {
|
||||
map.get(item.parentId)?.children.push(node);
|
||||
} else {
|
||||
|
||||
@@ -437,10 +437,14 @@ export class CategoriesService {
|
||||
// the groupId is a leaf-item reference, not a real sub-group ID), all returned
|
||||
// groups will match existing root-level category externalIds for this vehicle.
|
||||
// In that case, skip insertion — this category has no real sub-groups.
|
||||
if (!category.vehicleId) {
|
||||
// Catalog-only category (catalog vehicle) — root fallback irrelevant here
|
||||
return [];
|
||||
}
|
||||
const rootExtIds = await this.db
|
||||
.select({ externalId: categories.externalId })
|
||||
.from(categories)
|
||||
.where(eq(categories.vehicleId, category.vehicleId!));
|
||||
.where(eq(categories.vehicleId, category.vehicleId));
|
||||
const rootOnlyIds = rootExtIds
|
||||
.filter((r) => r.externalId)
|
||||
.map((r) => r.externalId as string);
|
||||
@@ -914,12 +918,11 @@ export class CategoriesService {
|
||||
description: p.description || null,
|
||||
quantity: p.quantity ? Number.parseInt(String(p.quantity), 10) || null : null,
|
||||
position: p.positionCode || null,
|
||||
hotspotIndex: p.hotspotId
|
||||
? (() => {
|
||||
const val = Number.parseInt(p.hotspotId!, 10);
|
||||
return val > 0 && val <= 2147483647 ? val : null;
|
||||
})()
|
||||
: null,
|
||||
hotspotIndex: ((): number | null => {
|
||||
if (!p.hotspotId) return null;
|
||||
const val = Number.parseInt(p.hotspotId, 10);
|
||||
return val > 0 && val <= 2147483647 ? val : null;
|
||||
})(),
|
||||
unavailable: p.unavailable || false,
|
||||
remark: p.remark || null,
|
||||
modelCodes: p.modelCodes || null,
|
||||
@@ -994,10 +997,10 @@ export class CategoriesService {
|
||||
`Failed to upload PSA schema image: ${(imgErr as Error).message}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
} else if (pl24Result.schemaImageUrl) {
|
||||
// P5 REST: download via getSchemaImage
|
||||
const imageResult = await this.pl24Service.getSchemaImage(
|
||||
pl24Result.schemaImageUrl!,
|
||||
pl24Result.schemaImageUrl,
|
||||
catalogInfo.serviceName,
|
||||
);
|
||||
|
||||
@@ -1217,7 +1220,8 @@ export class CategoriesService {
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
const node = map.get(item.id)!;
|
||||
const node = map.get(item.id);
|
||||
if (!node) continue;
|
||||
if (item.parentId && map.has(item.parentId)) {
|
||||
map.get(item.parentId)?.children.push(node);
|
||||
} else {
|
||||
|
||||
@@ -16,7 +16,8 @@ export type Database = PostgresJsDatabase<DatabaseSchema>;
|
||||
export const DatabaseProvider: Provider = {
|
||||
provide: DATABASE,
|
||||
useFactory: (configService: ConfigService): Database => {
|
||||
const databaseUrl = configService.get<string>("database.url")!;
|
||||
const databaseUrl = configService.get<string>("database.url");
|
||||
if (!databaseUrl) throw new Error("database.url is required");
|
||||
|
||||
const client = postgres(databaseUrl, {
|
||||
max: 20,
|
||||
|
||||
@@ -34,7 +34,7 @@ export class EmailService {
|
||||
}
|
||||
|
||||
async send(options: SendEmailOptions): Promise<void> {
|
||||
if (!this.isConfigured) {
|
||||
if (!this.postalApiUrl || !this.postalApiKey) {
|
||||
this.logger.log(`[DEV EMAIL] To: ${options.to}`);
|
||||
this.logger.log(`[DEV EMAIL] Subject: ${options.subject}`);
|
||||
this.logger.log(`[DEV EMAIL] Body: ${options.text || options.html.substring(0, 200)}`);
|
||||
@@ -55,7 +55,7 @@ export class EmailService {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Server-API-Key": this.postalApiKey!,
|
||||
"X-Server-API-Key": this.postalApiKey,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
@@ -106,7 +106,8 @@ export class EmexBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
await this.ensureBrowser();
|
||||
await this.ensureSession();
|
||||
|
||||
const page = await this.context!.newPage();
|
||||
if (!this.context) throw new Error("EMEX browser context not initialized");
|
||||
const page = await this.context.newPage();
|
||||
|
||||
let released = false;
|
||||
const release = async () => {
|
||||
@@ -230,14 +231,16 @@ export class EmexBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
if (Date.now() < this.sessionExpiry) return;
|
||||
|
||||
this.logger.log("Establishing EMEX session...");
|
||||
const page = await this.context!.newPage();
|
||||
if (!this.context) throw new Error("EMEX browser context not initialized");
|
||||
const ctx = this.context;
|
||||
const page = await ctx.newPage();
|
||||
try {
|
||||
await page.goto(EMEX_BASE_URL, {
|
||||
waitUntil: "networkidle",
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
const cookies = await this.context!.cookies();
|
||||
const cookies = await ctx.cookies();
|
||||
const session = cookies.find((c) => c.name === "ASP.NET_SessionId");
|
||||
if (session) {
|
||||
this.sessionExpiry = Date.now() + SESSION_TTL_MS;
|
||||
|
||||
@@ -645,7 +645,7 @@ export class EmexService {
|
||||
* Executes a promise with timeout
|
||||
*/
|
||||
private async executeWithTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
let timeoutId: NodeJS.Timeout | undefined;
|
||||
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
@@ -657,10 +657,10 @@ export class EmexService {
|
||||
|
||||
try {
|
||||
const result = await Promise.race([promise, timeoutPromise]);
|
||||
clearTimeout(timeoutId!);
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
return result;
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId!);
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,8 +347,15 @@ export class PartsCatalogsAuthService implements OnModuleInit, OnModuleDestroy {
|
||||
hour12: false,
|
||||
}).formatToParts(new Date());
|
||||
|
||||
const hour = Number.parseInt(parts.find((p) => p.type === "hour")!.value, 10);
|
||||
const minute = Number.parseInt(parts.find((p) => p.type === "minute")!.value, 10);
|
||||
const hourPart = parts.find((p) => p.type === "hour");
|
||||
const minutePart = parts.find((p) => p.type === "minute");
|
||||
if (!hourPart || !minutePart) {
|
||||
// Intl.DateTimeFormat with hour+minute always emits both parts; fall back to UTC if not.
|
||||
const now = new Date();
|
||||
return { hour: now.getUTCHours(), minute: now.getUTCMinutes() };
|
||||
}
|
||||
const hour = Number.parseInt(hourPart.value, 10);
|
||||
const minute = Number.parseInt(minutePart.value, 10);
|
||||
return { hour, minute };
|
||||
}
|
||||
|
||||
@@ -495,7 +502,8 @@ export class PartsCatalogsAuthService implements OnModuleInit, OnModuleDestroy {
|
||||
};
|
||||
}
|
||||
|
||||
context = await this.browser!.newContext(contextOptions);
|
||||
if (!this.browser) throw new Error("PCAT browser not initialized");
|
||||
context = await this.browser.newContext(contextOptions);
|
||||
const page = await context.newPage();
|
||||
|
||||
// Intercept the v3 widget call to /v3/api/proxy/* — needs the full
|
||||
@@ -516,8 +524,8 @@ export class PartsCatalogsAuthService implements OnModuleInit, OnModuleDestroy {
|
||||
apiPath: h["x-api-path"] || "https://api.parts-catalogs.com/v1",
|
||||
guiVersion: h["x-gui-version"] || "3",
|
||||
userId: h["x-user-id"] || "",
|
||||
origin: h["origin"] || "",
|
||||
referer: h["referer"] || "",
|
||||
origin: h.origin || "",
|
||||
referer: h.referer || "",
|
||||
};
|
||||
this.logger.debug(`Token intercepted (key=${apiKey.slice(0, 16)}...)`);
|
||||
});
|
||||
|
||||
@@ -74,12 +74,14 @@ export class PL24AuthService {
|
||||
if (!this.tokenData2 || !this.isTokenValid(this.tokenData2)) {
|
||||
await this.login2();
|
||||
}
|
||||
return this.tokenData2!.accessToken;
|
||||
if (!this.tokenData2) throw new Error("PL24 de account login failed");
|
||||
return this.tokenData2.accessToken;
|
||||
}
|
||||
if (!this.tokenData || !this.isTokenValid(this.tokenData)) {
|
||||
await this.login();
|
||||
}
|
||||
return this.tokenData!.accessToken;
|
||||
if (!this.tokenData) throw new Error("PL24 tr account login failed");
|
||||
return this.tokenData.accessToken;
|
||||
}
|
||||
|
||||
/** Return session cookie for the given account. */
|
||||
@@ -88,12 +90,14 @@ export class PL24AuthService {
|
||||
if (!this.tokenData2 || !this.isTokenValid(this.tokenData2)) {
|
||||
await this.login2();
|
||||
}
|
||||
return this.tokenData2!.sessionCookie;
|
||||
if (!this.tokenData2) throw new Error("PL24 de account login failed");
|
||||
return this.tokenData2.sessionCookie;
|
||||
}
|
||||
if (!this.tokenData || !this.isTokenValid(this.tokenData)) {
|
||||
await this.login();
|
||||
}
|
||||
return this.tokenData!.sessionCookie;
|
||||
if (!this.tokenData) throw new Error("PL24 tr account login failed");
|
||||
return this.tokenData.sessionCookie;
|
||||
}
|
||||
|
||||
/** Authorize a service catalog for the given account and return the service token. */
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* HTML parsing via regex + string extraction (no external deps like cheerio).
|
||||
*/
|
||||
|
||||
import { createHash } from "crypto";
|
||||
import { createHash } from "node:crypto";
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import type { ConfigService } from "@nestjs/config";
|
||||
import type { RedisService } from "../../redis/redis.service";
|
||||
@@ -515,8 +515,7 @@ export class PL24FordLegacyService {
|
||||
|
||||
for (const item of items) {
|
||||
// Avoid duplicating family name prefix (e.g. family="BERLINGO", salesType="BERLINGO VP" → "BERLINGO VP")
|
||||
const alreadyPrefixed =
|
||||
item.name && item.name.toUpperCase().startsWith(family.name.toUpperCase());
|
||||
const alreadyPrefixed = item.name?.toUpperCase().startsWith(family.name.toUpperCase());
|
||||
const modelLabel =
|
||||
item.name && !alreadyPrefixed && item.name !== family.name
|
||||
? `${family.name} ${item.name}`
|
||||
@@ -1629,9 +1628,9 @@ export class PL24FordLegacyService {
|
||||
async fetchFordMainGroups(
|
||||
serviceName: string,
|
||||
familyId: string,
|
||||
modelYear = "_all_",
|
||||
engine = "_all_",
|
||||
gearbox = "_all_",
|
||||
modelYear: string,
|
||||
engine: string,
|
||||
gearbox: string,
|
||||
mode: string,
|
||||
upds: string,
|
||||
catCode?: string,
|
||||
@@ -1660,30 +1659,14 @@ export class PL24FordLegacyService {
|
||||
|
||||
let url: string;
|
||||
if (arch === "LEGACY_HYUNDAI_KIA") {
|
||||
url =
|
||||
`${this.baseUrl}${basePath}/group.action` +
|
||||
`?catalog=${encodeURIComponent(familyId)}` +
|
||||
`&lang=tr&localMarketOnly=true&spec=e30%3D&startup=false` +
|
||||
`&mode=${mode}&upds=${upds}`;
|
||||
url = `${this.baseUrl}${basePath}/group.action?catalog=${encodeURIComponent(familyId)}&lang=tr&localMarketOnly=true&spec=e30%3D&startup=false&mode=${mode}&upds=${upds}`;
|
||||
} else if (arch === "LEGACY_NISSAN") {
|
||||
url =
|
||||
`${this.baseUrl}${basePath}/group.action` +
|
||||
`?model=${encodeURIComponent(familyId)}` +
|
||||
`&lang=tr&localMarketOnly=true&spec=e30%3D&startup=false` +
|
||||
`&mode=${mode}&upds=${upds}`;
|
||||
url = `${this.baseUrl}${basePath}/group.action?model=${encodeURIComponent(familyId)}&lang=tr&localMarketOnly=true&spec=e30%3D&startup=false&mode=${mode}&upds=${upds}`;
|
||||
} else if (arch === "LEGACY_OPEL") {
|
||||
url =
|
||||
`${this.baseUrl}${basePath}/group.action` +
|
||||
`?catId=${encodeURIComponent(familyId)}` +
|
||||
`&lang=tr&startup=false` +
|
||||
`&mode=${mode}&upds=${upds}`;
|
||||
url = `${this.baseUrl}${basePath}/group.action?catId=${encodeURIComponent(familyId)}&lang=tr&startup=false&mode=${mode}&upds=${upds}`;
|
||||
} else if (arch === "LEGACY_VOLVO") {
|
||||
const yearPart = modelYear !== "_all_" ? `&year=${encodeURIComponent(modelYear)}` : "";
|
||||
url =
|
||||
`${this.baseUrl}${basePath}/group.action` +
|
||||
`?mdl=${encodeURIComponent(familyId)}` +
|
||||
`&lang=tr&localMarketOnly=true&startup=false${yearPart}` +
|
||||
`&mode=${mode}&upds=${upds}`;
|
||||
url = `${this.baseUrl}${basePath}/group.action?mdl=${encodeURIComponent(familyId)}&lang=tr&localMarketOnly=true&startup=false${yearPart}&mode=${mode}&upds=${upds}`;
|
||||
} else if (catCode) {
|
||||
// LEGACY_FORD with catCode: catCode is the generation identifier required by group.action.
|
||||
// The json-model-config.action also uses catCode to get engine options for this generation.
|
||||
@@ -1694,14 +1677,7 @@ export class PL24FordLegacyService {
|
||||
`&mode=${mode}&upds=${upds}`;
|
||||
} else {
|
||||
// LEGACY_FORD fallback (no catCode): old family+variant style — may return 500 for some families
|
||||
url =
|
||||
`${this.baseUrl}${basePath}/group.action` +
|
||||
`?modelFamily=${encodeURIComponent(familyId)}` +
|
||||
`&modelYear=${encodeURIComponent(modelYear)}` +
|
||||
`&engine=${encodeURIComponent(engine)}` +
|
||||
`&gearbox=${encodeURIComponent(gearbox)}` +
|
||||
`&lang=tr&startup=false` +
|
||||
`&mode=${mode}&upds=${upds}`;
|
||||
url = `${this.baseUrl}${basePath}/group.action?modelFamily=${encodeURIComponent(familyId)}&modelYear=${encodeURIComponent(modelYear)}&engine=${encodeURIComponent(engine)}&gearbox=${encodeURIComponent(gearbox)}&lang=tr&startup=false&mode=${mode}&upds=${upds}`;
|
||||
}
|
||||
|
||||
const html = await this.fetchP4Page(url, serviceName, true);
|
||||
@@ -1939,11 +1915,14 @@ export class PL24FordLegacyService {
|
||||
}
|
||||
}
|
||||
|
||||
if (!vehicle.catalogInfo) {
|
||||
throw new Error("PL24 PSA decode: vehicle.catalogInfo missing — required for psa flow");
|
||||
}
|
||||
const result: PL24DecodedVehicle = {
|
||||
...vehicle,
|
||||
categories: scopes,
|
||||
catalogInfo: {
|
||||
...vehicle.catalogInfo!,
|
||||
...vehicle.catalogInfo,
|
||||
psaFamilyId: familyId || undefined,
|
||||
psaSalesTypeId: salesTypeId || undefined,
|
||||
psaMode: mode,
|
||||
@@ -2163,8 +2142,8 @@ export class PL24FordLegacyService {
|
||||
return code && code !== "_all_" && !it.subheader && !it.gray;
|
||||
})
|
||||
.map((it) => ({
|
||||
code: ((it[codeKey] as string) || (it.code as string))!,
|
||||
name: ((it[nameKey] as string) || (it.name as string) || "")!,
|
||||
code: (it[codeKey] as string) || (it.code as string),
|
||||
name: (it[nameKey] as string) || (it.name as string) || "",
|
||||
}));
|
||||
} catch {
|
||||
return [];
|
||||
|
||||
@@ -30,10 +30,11 @@ export async function processEmexScrape(
|
||||
console.log(`[emex-scrape] Processing job ${job.id} for VIN: ${vin}, user: ${userId}`);
|
||||
|
||||
// Update scrape session to active
|
||||
if (!job.id) throw new Error("BullMQ job missing id");
|
||||
const [session] = await db
|
||||
.select()
|
||||
.from(emexScrapeSessions)
|
||||
.where(eq(emexScrapeSessions.jobId, job.id!))
|
||||
.where(eq(emexScrapeSessions.jobId, job.id))
|
||||
.limit(1);
|
||||
|
||||
if (session) {
|
||||
|
||||
@@ -61,12 +61,11 @@ export class PartsService {
|
||||
description: p.description || null,
|
||||
quantity: p.quantity ? Number.parseInt(String(p.quantity), 10) || null : null,
|
||||
position: p.positionCode || null,
|
||||
hotspotIndex: p.hotspotId
|
||||
? (() => {
|
||||
const val = Number.parseInt(p.hotspotId!, 10);
|
||||
return val > 0 && val <= 2147483647 ? val : null;
|
||||
})()
|
||||
: null,
|
||||
hotspotIndex: ((): number | null => {
|
||||
if (!p.hotspotId) return null;
|
||||
const val = Number.parseInt(p.hotspotId, 10);
|
||||
return val > 0 && val <= 2147483647 ? val : null;
|
||||
})(),
|
||||
unavailable: p.unavailable || false,
|
||||
remark: p.remark || null,
|
||||
modelCodes: p.modelCodes || null,
|
||||
|
||||
@@ -15,22 +15,27 @@ export class StorageService {
|
||||
private readonly publicUrl: string;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
const endpoint = configService.get<string>("minio.endpoint")!;
|
||||
const endpoint = configService.get<string>("minio.endpoint");
|
||||
const accessKeyId = configService.get<string>("minio.accessKey");
|
||||
const secretAccessKey = configService.get<string>("minio.secretKey");
|
||||
const publicUrl = configService.get<string>("minio.publicUrl");
|
||||
if (!endpoint || !accessKeyId || !secretAccessKey || !publicUrl) {
|
||||
throw new Error(
|
||||
"Storage requires minio.endpoint, accessKey, secretKey, and publicUrl",
|
||||
);
|
||||
}
|
||||
const useSSL = configService.get<boolean>("minio.useSSL", false);
|
||||
|
||||
this.s3 = new S3Client({
|
||||
endpoint,
|
||||
region: "us-east-1",
|
||||
credentials: {
|
||||
accessKeyId: configService.get<string>("minio.accessKey")!,
|
||||
secretAccessKey: configService.get<string>("minio.secretKey")!,
|
||||
},
|
||||
credentials: { accessKeyId, secretAccessKey },
|
||||
forcePathStyle: true,
|
||||
...(useSSL ? {} : { tls: false }),
|
||||
});
|
||||
|
||||
this.bucketName = configService.get<string>("minio.bucketName", "sase-schemas");
|
||||
this.publicUrl = configService.get<string>("minio.publicUrl")!;
|
||||
this.publicUrl = publicUrl;
|
||||
}
|
||||
|
||||
async upload(key: string, body: Buffer | Uint8Array, contentType: string): Promise<string> {
|
||||
|
||||
@@ -32,7 +32,7 @@ export function PsaVariantSelector({ vehicleId, onSelect }: PsaVariantSelectorPr
|
||||
queryKey: ["psa-engines", vehicleId, selectedBody],
|
||||
queryFn: () =>
|
||||
api.get<VariantItem[]>(
|
||||
`/catalog/vehicles/${vehicleId}/psa-engines?body=${encodeURIComponent(selectedBody!)}`,
|
||||
`/catalog/vehicles/${vehicleId}/psa-engines?body=${encodeURIComponent(selectedBody ?? "")}`,
|
||||
),
|
||||
enabled: !!vehicleId && !!selectedBody,
|
||||
});
|
||||
@@ -41,7 +41,7 @@ export function PsaVariantSelector({ vehicleId, onSelect }: PsaVariantSelectorPr
|
||||
queryKey: ["psa-gearboxes", vehicleId, selectedBody, selectedEngine],
|
||||
queryFn: () =>
|
||||
api.get<VariantItem[]>(
|
||||
`/catalog/vehicles/${vehicleId}/psa-gearboxes?body=${encodeURIComponent(selectedBody!)}&engine=${encodeURIComponent(selectedEngine!)}`,
|
||||
`/catalog/vehicles/${vehicleId}/psa-gearboxes?body=${encodeURIComponent(selectedBody ?? "")}&engine=${encodeURIComponent(selectedEngine ?? "")}`,
|
||||
),
|
||||
enabled: !!vehicleId && !!selectedBody && !!selectedEngine,
|
||||
});
|
||||
@@ -58,7 +58,8 @@ export function PsaVariantSelector({ vehicleId, onSelect }: PsaVariantSelectorPr
|
||||
|
||||
const handleEngineSelect = (code: string | "_all_") => {
|
||||
if (code === "_all_") {
|
||||
onSelect(selectedBody!, "_all_", "_all_");
|
||||
if (!selectedBody) return;
|
||||
onSelect(selectedBody, "_all_", "_all_");
|
||||
return;
|
||||
}
|
||||
setSelectedEngine(code);
|
||||
@@ -67,7 +68,8 @@ export function PsaVariantSelector({ vehicleId, onSelect }: PsaVariantSelectorPr
|
||||
|
||||
const handleGearboxSelect = (code: string | "_all_") => {
|
||||
if (code === "_all_") {
|
||||
onSelect(selectedBody!, selectedEngine!, "_all_");
|
||||
if (!selectedBody || !selectedEngine) return;
|
||||
onSelect(selectedBody, selectedEngine, "_all_");
|
||||
return;
|
||||
}
|
||||
setSelectedGearbox(code);
|
||||
|
||||
@@ -120,6 +120,12 @@ export function PartsPanel({ parts, vehicleId, categoryId }: PartsPanelProps) {
|
||||
onMouseEnter={() => setHighlightedGroup(group)}
|
||||
onMouseLeave={() => setHighlightedGroup(null)}
|
||||
onClick={() => setSelectedGroup(selectedGroup === group ? null : group)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
setSelectedGroup(selectedGroup === group ? null : group);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<td className="px-3 py-2 text-muted-foreground">{part.hotspotIndex}</td>
|
||||
<td className="px-3 py-2">
|
||||
|
||||
@@ -461,7 +461,15 @@ function AdminUsersPage() {
|
||||
<div
|
||||
key={u.id}
|
||||
className="grid cursor-pointer items-center gap-4 px-6 py-4 transition-colors hover:bg-muted/50 lg:grid-cols-7"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setSelectedUserId(u.id)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
setSelectedUserId(u.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium">{u.name}</p>
|
||||
|
||||
@@ -134,9 +134,9 @@ function CatalogModelsPage() {
|
||||
<Skeleton key={__k} className="h-24 w-full rounded-xl" />
|
||||
))}
|
||||
</div>
|
||||
) : isMultiCatalog && !activeCatalog ? (
|
||||
) : isMultiCatalog && !activeCatalog && catalogs ? (
|
||||
// Sub-catalog selector
|
||||
<CatalogSelector catalogs={catalogs!} brandName={brandName} brandLabel={decodedBrandName} />
|
||||
<CatalogSelector catalogs={catalogs} brandName={brandName} brandLabel={decodedBrandName} />
|
||||
) : modelsLoading ? (
|
||||
<div>
|
||||
<div className="mb-4 flex items-center gap-2 text-sm text-muted-foreground">
|
||||
|
||||
@@ -143,12 +143,13 @@ function VehicleCategoryPage() {
|
||||
|
||||
{/* Parent category — show children */}
|
||||
{hasChildren &&
|
||||
data?.children &&
|
||||
(viewMode === "grid" ? (
|
||||
<CategoryGrid categories={data.children!} vehicleId={id} />
|
||||
<CategoryGrid categories={data.children} vehicleId={id} />
|
||||
) : viewMode === "tree" ? (
|
||||
<CategoryTree categories={data.children!} vehicleId={id} />
|
||||
<CategoryTree categories={data.children} vehicleId={id} />
|
||||
) : (
|
||||
<CategoryColumns categories={data.children!} vehicleId={id} />
|
||||
<CategoryColumns categories={data.children} vehicleId={id} />
|
||||
))}
|
||||
|
||||
{/* Leaf category — show schema viewer */}
|
||||
|
||||
@@ -620,7 +620,7 @@ function HomePage() {
|
||||
>
|
||||
{isDark ? <Sun className="size-4" /> : <Moon className="size-4" />}
|
||||
</button>
|
||||
<button onClick={() => setMobileMenuOpen(!mobileMenuOpen)}>
|
||||
<button type="button" onClick={() => setMobileMenuOpen(!mobileMenuOpen)}>
|
||||
{mobileMenuOpen ? <X className="size-6" /> : <Menu className="size-6" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user