feat(frontend): auto-generate TypeScript types from backend OpenAPI schema
Replace manually maintained types in src/lib/types.ts with auto-generated types from FastAPI's OpenAPI schema using @hey-api/openapi-ts. The bridge file re-exports generated types with renames, Require<> augmentations for fields that are optional in the schema but always present in responses, and manually added relationship fields excluded from OpenAPI. - Add openapi-ts.config.ts and generate:api npm script - Generate types into src/lib/api/generated/types.gen.ts - Rewrite src/lib/types.ts as bridge with re-exports and augmentations - Fix null vs undefined mismatches in consumer components - Remove unused manual type definitions from api.ts - Update AGENTS.md docs with type generation workflow
This commit is contained in:
parent
470d49b061
commit
e29d62f949
16 changed files with 13745 additions and 2298 deletions
|
|
@ -1,22 +1,22 @@
|
|||
import { browser } from "$app/environment";
|
||||
import { PUBLIC_API_BASE } from "$env/static/public";
|
||||
import type {
|
||||
ActiveIngredient,
|
||||
BatchSuggestion,
|
||||
GroomingSchedule,
|
||||
LabResult,
|
||||
LabResultListResponse,
|
||||
MedicationEntry,
|
||||
MedicationUsage,
|
||||
PartOfDay,
|
||||
Product,
|
||||
ProductSummary,
|
||||
ProductContext,
|
||||
ProductEffectProfile,
|
||||
ProductInventory,
|
||||
ProductParseResponse,
|
||||
ProductSummary,
|
||||
Routine,
|
||||
RoutineSuggestion,
|
||||
RoutineStep,
|
||||
SkinConditionSnapshot,
|
||||
SkinPhotoAnalysisResponse,
|
||||
UserProfile,
|
||||
} from "./types";
|
||||
|
||||
|
|
@ -120,41 +120,6 @@ export const updateInventory = (
|
|||
export const deleteInventory = (id: string): Promise<void> =>
|
||||
api.del(`/inventory/${id}`);
|
||||
|
||||
export interface ProductParseResponse {
|
||||
name?: string;
|
||||
brand?: string;
|
||||
line_name?: string;
|
||||
sku?: string;
|
||||
url?: string;
|
||||
barcode?: string;
|
||||
category?: string;
|
||||
recommended_time?: string;
|
||||
texture?: string;
|
||||
absorption_speed?: string;
|
||||
leave_on?: boolean;
|
||||
price_amount?: number;
|
||||
price_currency?: string;
|
||||
size_ml?: number;
|
||||
pao_months?: number;
|
||||
inci?: string[];
|
||||
actives?: ActiveIngredient[];
|
||||
recommended_for?: string[];
|
||||
targets?: string[];
|
||||
fragrance_free?: boolean;
|
||||
essential_oils_free?: boolean;
|
||||
alcohol_denat_free?: boolean;
|
||||
pregnancy_safe?: boolean;
|
||||
product_effect_profile?: ProductEffectProfile;
|
||||
ph_min?: number;
|
||||
ph_max?: number;
|
||||
context_rules?: ProductContext;
|
||||
min_interval_hours?: number;
|
||||
max_frequency_per_week?: number;
|
||||
is_medication?: boolean;
|
||||
is_tool?: boolean;
|
||||
needle_length_mm?: number;
|
||||
}
|
||||
|
||||
export const parseProductText = (text: string): Promise<ProductParseResponse> =>
|
||||
api.post("/products/parse-text", { text });
|
||||
|
||||
|
|
@ -283,13 +248,6 @@ export interface LabResultListParams {
|
|||
offset?: number;
|
||||
}
|
||||
|
||||
export interface LabResultListResponse {
|
||||
items: LabResult[];
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
export function getLabResults(
|
||||
params: LabResultListParams = {},
|
||||
): Promise<LabResultListResponse> {
|
||||
|
|
@ -353,21 +311,6 @@ export const updateSkinSnapshot = (
|
|||
export const deleteSkinSnapshot = (id: string): Promise<void> =>
|
||||
api.del(`/skincare/${id}`);
|
||||
|
||||
export interface SkinPhotoAnalysisResponse {
|
||||
overall_state?: string;
|
||||
texture?: string;
|
||||
skin_type?: string;
|
||||
hydration_level?: number;
|
||||
sebum_tzone?: number;
|
||||
sebum_cheeks?: number;
|
||||
sensitivity_level?: number;
|
||||
barrier_state?: string;
|
||||
active_concerns?: string[];
|
||||
risks?: string[];
|
||||
priorities?: string[];
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export async function analyzeSkinPhotos(
|
||||
files: File[],
|
||||
): Promise<SkinPhotoAnalysisResponse> {
|
||||
|
|
|
|||
3
frontend/src/lib/api/generated/index.ts
Normal file
3
frontend/src/lib/api/generated/index.ts
Normal file
File diff suppressed because one or more lines are too long
3922
frontend/src/lib/api/generated/types.gen.ts
Normal file
3922
frontend/src/lib/api/generated/types.gen.ts
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,14 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import type { Product } from '$lib/types';
|
||||
import type { IngredientFunction } from '$lib/types';
|
||||
import type { Product, IngredientFunction, ProductParseResponse } from '$lib/types';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Tabs, TabsList, TabsTrigger } from '$lib/components/ui/tabs';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||
import { baseSelectClass, baseTextareaClass } from '$lib/components/forms/form-classes';
|
||||
import type { ProductParseResponse } from '$lib/api';
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
import { Sparkles, X } from 'lucide-svelte';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,390 +1,112 @@
|
|||
// ─── Enums ──────────────────────────────────────────────────────────────────
|
||||
// Re-exports from generated OpenAPI types.
|
||||
// This bridge file keeps `$lib/types` as the canonical import path while
|
||||
// renaming backend response-model names to shorter frontend-friendly names
|
||||
// and augmenting types where the OpenAPI schema diverges from runtime:
|
||||
// - Relationship fields (SQLModel Relationships excluded from schema)
|
||||
// - Default-valued fields marked optional in schema but always present in responses
|
||||
|
||||
export type AbsorptionSpeed =
|
||||
| "very_fast"
|
||||
| "fast"
|
||||
| "moderate"
|
||||
| "slow"
|
||||
| "very_slow";
|
||||
export type BarrierState = "intact" | "mildly_compromised" | "compromised";
|
||||
export type DayTime = "am" | "pm" | "both";
|
||||
export type GroomingAction =
|
||||
| "shaving_razor"
|
||||
| "shaving_oneblade"
|
||||
| "dermarolling";
|
||||
export type IngredientFunction =
|
||||
| "humectant"
|
||||
| "emollient"
|
||||
| "occlusive"
|
||||
| "exfoliant_aha"
|
||||
| "exfoliant_bha"
|
||||
| "exfoliant_pha"
|
||||
| "retinoid"
|
||||
| "antioxidant"
|
||||
| "soothing"
|
||||
| "barrier_support"
|
||||
| "brightening"
|
||||
| "anti_acne"
|
||||
| "ceramide"
|
||||
| "niacinamide"
|
||||
| "sunscreen"
|
||||
| "peptide"
|
||||
| "hair_growth_stimulant"
|
||||
| "prebiotic"
|
||||
| "vitamin_c"
|
||||
| "anti_aging";
|
||||
export type MedicationKind =
|
||||
| "prescription"
|
||||
| "otc"
|
||||
| "supplement"
|
||||
| "herbal"
|
||||
| "other";
|
||||
export type OverallSkinState = "excellent" | "good" | "fair" | "poor";
|
||||
export type PartOfDay = "am" | "pm";
|
||||
export type PriceTier = "budget" | "mid" | "premium" | "luxury";
|
||||
import type {
|
||||
GroomingSchedule as _GroomingSchedule,
|
||||
LabResult as _LabResult,
|
||||
LabResultListResponse as _LabResultListResponse,
|
||||
MedicationEntry as _MedicationEntry,
|
||||
MedicationUsage as _MedicationUsage,
|
||||
ProductInventory as _ProductInventory,
|
||||
ProductListItem as _ProductListItem,
|
||||
ProductWithInventory as _ProductWithInventory,
|
||||
Routine as _Routine,
|
||||
RoutineStep as _RoutineStep,
|
||||
SkinConditionSnapshotPublic as _SkinConditionSnapshotPublic,
|
||||
} from "./api/generated/types.gen";
|
||||
|
||||
type Require<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
|
||||
|
||||
export type {
|
||||
AbsorptionSpeed,
|
||||
ActiveIngredient,
|
||||
BarrierState,
|
||||
BatchSuggestion,
|
||||
DayPlan,
|
||||
DayTime,
|
||||
GroomingAction,
|
||||
IngredientFunction,
|
||||
MedicationKind,
|
||||
OverallSkinState,
|
||||
PartOfDay,
|
||||
PriceTier,
|
||||
ProductCategory,
|
||||
ProductContext,
|
||||
ProductEffectProfile,
|
||||
ProductParseResponse,
|
||||
ProductSuggestion,
|
||||
RemainingLevel,
|
||||
ResponseMetadata,
|
||||
ResultFlag,
|
||||
RoutineSuggestion,
|
||||
RoutineSuggestionSummary,
|
||||
SexAtBirth,
|
||||
ShoppingSuggestionResponse,
|
||||
SkinConcern,
|
||||
SkinPhotoAnalysisResponse,
|
||||
SkinTexture,
|
||||
SkinType,
|
||||
StrengthLevel,
|
||||
SuggestedStep,
|
||||
TextureType,
|
||||
TokenMetrics,
|
||||
} from "./api/generated/types.gen";
|
||||
|
||||
export type { ProductWithInventory } from "./api/generated/types.gen";
|
||||
export type { UserProfilePublic as UserProfile } from "./api/generated/types.gen";
|
||||
|
||||
// Inline literal types in backend — not named schemas in OpenAPI
|
||||
export type PriceTierSource = "category" | "fallback" | "insufficient_data";
|
||||
export type RemainingLevel = "high" | "medium" | "low" | "nearly_empty";
|
||||
export type ProductCategory =
|
||||
| "cleanser"
|
||||
| "toner"
|
||||
| "essence"
|
||||
| "serum"
|
||||
| "moisturizer"
|
||||
| "spf"
|
||||
| "mask"
|
||||
| "exfoliant"
|
||||
| "hair_treatment"
|
||||
| "tool"
|
||||
| "spot_treatment"
|
||||
| "oil";
|
||||
export type ResultFlag = "N" | "ABN" | "POS" | "NEG" | "L" | "H";
|
||||
export type SkinConcern =
|
||||
| "acne"
|
||||
| "rosacea"
|
||||
| "hyperpigmentation"
|
||||
| "aging"
|
||||
| "dehydration"
|
||||
| "redness"
|
||||
| "damaged_barrier"
|
||||
| "pore_visibility"
|
||||
| "uneven_texture"
|
||||
| "hair_growth"
|
||||
| "sebum_excess";
|
||||
export type SkinTexture = "smooth" | "rough" | "flaky" | "bumpy";
|
||||
export type SexAtBirth = "male" | "female" | "intersex";
|
||||
export type SkinType =
|
||||
| "dry"
|
||||
| "oily"
|
||||
| "combination"
|
||||
| "sensitive"
|
||||
| "normal"
|
||||
| "acne_prone";
|
||||
export type StrengthLevel = 1 | 2 | 3;
|
||||
export type TextureType =
|
||||
| "watery"
|
||||
| "gel"
|
||||
| "emulsion"
|
||||
| "cream"
|
||||
| "oil"
|
||||
| "balm"
|
||||
| "foam"
|
||||
| "fluid";
|
||||
// ─── Product types ───────────────────────────────────────────────────────────
|
||||
export type ShoppingPriority = "high" | "medium" | "low";
|
||||
|
||||
export interface ActiveIngredient {
|
||||
name: string;
|
||||
percent?: number;
|
||||
functions: IngredientFunction[];
|
||||
strength_level?: StrengthLevel;
|
||||
irritation_potential?: StrengthLevel;
|
||||
}
|
||||
// Response types: required fields + relationship augmentations.
|
||||
|
||||
export interface ProductEffectProfile {
|
||||
hydration_immediate: number;
|
||||
hydration_long_term: number;
|
||||
barrier_repair_strength: number;
|
||||
soothing_strength: number;
|
||||
exfoliation_strength: number;
|
||||
retinoid_strength: number;
|
||||
irritation_risk: number;
|
||||
comedogenic_risk: number;
|
||||
barrier_disruption_risk: number;
|
||||
dryness_risk: number;
|
||||
brightening_strength: number;
|
||||
anti_acne_strength: number;
|
||||
anti_aging_strength: number;
|
||||
}
|
||||
|
||||
export interface ProductContext {
|
||||
safe_after_shaving?: boolean;
|
||||
safe_after_acids?: boolean;
|
||||
safe_after_retinoids?: boolean;
|
||||
safe_with_compromised_barrier?: boolean;
|
||||
low_uv_only?: boolean;
|
||||
}
|
||||
|
||||
export interface ProductInventory {
|
||||
id: string;
|
||||
product_id: string;
|
||||
is_opened: boolean;
|
||||
opened_at?: string;
|
||||
finished_at?: string;
|
||||
expiry_date?: string;
|
||||
remaining_level?: RemainingLevel;
|
||||
notes?: string;
|
||||
created_at: string;
|
||||
product?: Product;
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
id: string;
|
||||
name: string;
|
||||
brand: string;
|
||||
line_name?: string;
|
||||
sku?: string;
|
||||
url?: string;
|
||||
barcode?: string;
|
||||
category: ProductCategory;
|
||||
recommended_time: DayTime;
|
||||
texture?: TextureType;
|
||||
absorption_speed?: AbsorptionSpeed;
|
||||
leave_on: boolean;
|
||||
price_amount?: number;
|
||||
price_currency?: string;
|
||||
price_tier?: PriceTier;
|
||||
price_per_use_pln?: number;
|
||||
price_tier_source?: PriceTierSource;
|
||||
size_ml?: number;
|
||||
pao_months?: number;
|
||||
inci: string[];
|
||||
actives?: ActiveIngredient[];
|
||||
recommended_for: SkinType[];
|
||||
targets: SkinConcern[];
|
||||
fragrance_free?: boolean;
|
||||
essential_oils_free?: boolean;
|
||||
alcohol_denat_free?: boolean;
|
||||
pregnancy_safe?: boolean;
|
||||
product_effect_profile: ProductEffectProfile;
|
||||
ph_min?: number;
|
||||
ph_max?: number;
|
||||
context_rules?: ProductContext;
|
||||
min_interval_hours?: number;
|
||||
max_frequency_per_week?: number;
|
||||
is_medication: boolean;
|
||||
is_tool: boolean;
|
||||
needle_length_mm?: number;
|
||||
personal_tolerance_notes?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
export type Product = Require<
|
||||
_ProductWithInventory,
|
||||
"id" | "created_at" | "updated_at" | "inventory" | "inci" | "recommended_for" | "targets"
|
||||
> & {
|
||||
inventory: ProductInventory[];
|
||||
}
|
||||
};
|
||||
|
||||
export interface ProductSummary {
|
||||
id: string;
|
||||
name: string;
|
||||
brand: string;
|
||||
category: ProductCategory;
|
||||
recommended_time: DayTime;
|
||||
targets: SkinConcern[];
|
||||
is_owned: boolean;
|
||||
price_tier?: PriceTier;
|
||||
price_per_use_pln?: number;
|
||||
price_tier_source?: PriceTierSource;
|
||||
}
|
||||
export type ProductSummary = Require<_ProductListItem, "id" | "targets">;
|
||||
|
||||
// ─── Routine types ───────────────────────────────────────────────────────────
|
||||
export type SkinConditionSnapshot = Require<
|
||||
_SkinConditionSnapshotPublic,
|
||||
"id" | "created_at" | "active_concerns" | "risks" | "priorities"
|
||||
>;
|
||||
|
||||
export interface RoutineStep {
|
||||
id: string;
|
||||
routine_id: string;
|
||||
product_id?: string;
|
||||
order_index: number;
|
||||
action_type?: GroomingAction;
|
||||
action_notes?: string;
|
||||
dose?: string;
|
||||
region?: string;
|
||||
product?: Product;
|
||||
}
|
||||
export type GroomingSchedule = Require<_GroomingSchedule, "id">;
|
||||
|
||||
export interface Routine {
|
||||
id: string;
|
||||
routine_date: string;
|
||||
part_of_day: PartOfDay;
|
||||
notes?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
steps?: RoutineStep[];
|
||||
}
|
||||
export type LabResult = Require<_LabResult, "record_id" | "created_at" | "updated_at">;
|
||||
|
||||
export interface GroomingSchedule {
|
||||
id: string;
|
||||
day_of_week: number;
|
||||
action: GroomingAction;
|
||||
notes?: string;
|
||||
}
|
||||
export type LabResultListResponse = Omit<_LabResultListResponse, "items"> & {
|
||||
items: LabResult[];
|
||||
};
|
||||
|
||||
export interface SuggestedStep {
|
||||
product_id?: string;
|
||||
action_type?: GroomingAction;
|
||||
action_notes?: string;
|
||||
region?: string;
|
||||
why_this_step?: string;
|
||||
optional?: boolean;
|
||||
}
|
||||
export type MedicationUsage = Require<
|
||||
_MedicationUsage,
|
||||
"record_id" | "created_at" | "updated_at"
|
||||
>;
|
||||
|
||||
export interface RoutineSuggestionSummary {
|
||||
primary_goal: string;
|
||||
constraints_applied: string[];
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
// Phase 3: Observability metadata types
|
||||
export interface TokenMetrics {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
thoughts_tokens?: number;
|
||||
total_tokens: number;
|
||||
}
|
||||
|
||||
export interface ResponseMetadata {
|
||||
model_used: string;
|
||||
duration_ms: number;
|
||||
reasoning_chain?: string;
|
||||
token_metrics?: TokenMetrics;
|
||||
}
|
||||
|
||||
export interface RoutineSuggestion {
|
||||
steps: SuggestedStep[];
|
||||
reasoning: string;
|
||||
summary?: RoutineSuggestionSummary;
|
||||
// Phase 3: Observability fields
|
||||
validation_warnings?: string[];
|
||||
auto_fixes_applied?: string[];
|
||||
response_metadata?: ResponseMetadata;
|
||||
}
|
||||
|
||||
export interface DayPlan {
|
||||
date: string;
|
||||
am_steps: SuggestedStep[];
|
||||
pm_steps: SuggestedStep[];
|
||||
reasoning: string;
|
||||
}
|
||||
|
||||
export interface BatchSuggestion {
|
||||
days: DayPlan[];
|
||||
overall_reasoning: string;
|
||||
// Phase 3: Observability fields
|
||||
validation_warnings?: string[];
|
||||
auto_fixes_applied?: string[];
|
||||
response_metadata?: ResponseMetadata;
|
||||
}
|
||||
|
||||
// ─── Shopping suggestion types ───────────────────────────────────────────────
|
||||
|
||||
export type ShoppingPriority = 'high' | 'medium' | 'low';
|
||||
|
||||
export interface ProductSuggestion {
|
||||
category: string;
|
||||
product_type: string;
|
||||
priority: ShoppingPriority;
|
||||
key_ingredients: string[];
|
||||
target_concerns: string[];
|
||||
recommended_time: string;
|
||||
frequency: string;
|
||||
short_reason: string;
|
||||
reason_to_buy_now: string;
|
||||
reason_not_needed_if_budget_tight?: string;
|
||||
fit_with_current_routine: string;
|
||||
usage_cautions: string[];
|
||||
}
|
||||
|
||||
export interface ShoppingSuggestionResponse {
|
||||
suggestions: ProductSuggestion[];
|
||||
reasoning: string;
|
||||
// Phase 3: Observability fields
|
||||
validation_warnings?: string[];
|
||||
auto_fixes_applied?: string[];
|
||||
response_metadata?: ResponseMetadata;
|
||||
}
|
||||
|
||||
// ─── Health types ────────────────────────────────────────────────────────────
|
||||
|
||||
export interface MedicationUsage {
|
||||
record_id: string;
|
||||
medication_record_id: string;
|
||||
dose_value?: number;
|
||||
dose_unit?: string;
|
||||
frequency?: string;
|
||||
schedule_text?: string;
|
||||
as_needed: boolean;
|
||||
valid_from: string;
|
||||
valid_to?: string;
|
||||
source_file?: string;
|
||||
notes?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface MedicationEntry {
|
||||
record_id: string;
|
||||
kind: MedicationKind;
|
||||
product_name: string;
|
||||
active_substance?: string;
|
||||
formulation?: string;
|
||||
route?: string;
|
||||
source_file?: string;
|
||||
notes?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
export type MedicationEntry = Require<
|
||||
_MedicationEntry,
|
||||
"record_id" | "created_at" | "updated_at"
|
||||
> & {
|
||||
usage_history: MedicationUsage[];
|
||||
}
|
||||
};
|
||||
|
||||
export interface LabResult {
|
||||
record_id: string;
|
||||
collected_at: string;
|
||||
test_code: string;
|
||||
test_name_original?: string;
|
||||
test_name_loinc?: string;
|
||||
value_num?: number;
|
||||
value_text?: string;
|
||||
value_bool?: boolean;
|
||||
unit_original?: string;
|
||||
unit_ucum?: string;
|
||||
ref_low?: number;
|
||||
ref_high?: number;
|
||||
ref_text?: string;
|
||||
flag?: ResultFlag;
|
||||
lab?: string;
|
||||
source_file?: string;
|
||||
notes?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
export type ProductInventory = Require<_ProductInventory, "id" | "created_at"> & {
|
||||
product?: _ProductWithInventory;
|
||||
};
|
||||
|
||||
// ─── Skin types ──────────────────────────────────────────────────────────────
|
||||
export type Routine = Require<_Routine, "id" | "created_at" | "updated_at"> & {
|
||||
steps?: RoutineStep[];
|
||||
};
|
||||
|
||||
export interface SkinConditionSnapshot {
|
||||
id: string;
|
||||
snapshot_date: string;
|
||||
overall_state?: OverallSkinState;
|
||||
skin_type?: SkinType;
|
||||
texture?: SkinTexture;
|
||||
hydration_level?: number;
|
||||
sebum_tzone?: number;
|
||||
sebum_cheeks?: number;
|
||||
sensitivity_level?: number;
|
||||
barrier_state?: BarrierState;
|
||||
active_concerns: SkinConcern[];
|
||||
risks: string[];
|
||||
priorities: string[];
|
||||
notes?: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
id: string;
|
||||
birth_date?: string;
|
||||
sex_at_birth?: SexAtBirth;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
export type RoutineStep = Require<_RoutineStep, "id"> & {
|
||||
product?: _ProductWithInventory;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@
|
|||
return a.localeCompare(b, undefined, { sensitivity: 'base' });
|
||||
}
|
||||
|
||||
function comparePrice(a?: number, b?: number): number {
|
||||
function comparePrice(a?: number | null, b?: number | null): number {
|
||||
if (a == null && b == null) return 0;
|
||||
if (a == null) return 1;
|
||||
if (b == null) return -1;
|
||||
|
|
@ -109,12 +109,12 @@
|
|||
const totalCount = $derived(groupedProducts.reduce((s, [, arr]) => s + arr.length, 0));
|
||||
const ownedCount = $derived(data.products.filter((product) => product.is_owned).length);
|
||||
|
||||
function formatPricePerUse(value?: number): string {
|
||||
function formatPricePerUse(value?: number | null): string {
|
||||
if (value == null) return '-';
|
||||
return `${value.toFixed(2)} ${m.common_pricePerUse()}`;
|
||||
}
|
||||
|
||||
function formatTier(value?: string): string {
|
||||
function formatTier(value?: string | null): string {
|
||||
if (!value) return m.common_unknown();
|
||||
if (value === 'budget') return m['productForm_priceBudget']();
|
||||
if (value === 'mid') return m['productForm_priceMid']();
|
||||
|
|
@ -123,7 +123,7 @@
|
|||
return value;
|
||||
}
|
||||
|
||||
function getPricePerUse(product: ProductSummary): number | undefined {
|
||||
function getPricePerUse(product: ProductSummary): number | null | undefined {
|
||||
return product.price_per_use_pln;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@
|
|||
return (product as { price_per_use_pln?: number }).price_per_use_pln;
|
||||
}
|
||||
|
||||
function formatTier(value?: string): string {
|
||||
function formatTier(value?: string | null): string {
|
||||
if (!value) return m.common_unknown();
|
||||
if (value === 'budget') return m['productForm_priceBudget']();
|
||||
if (value === 'mid') return m['productForm_priceMid']();
|
||||
|
|
@ -248,7 +248,7 @@
|
|||
editingInventoryId = null;
|
||||
} else {
|
||||
editingInventoryId = pkg.id;
|
||||
editingInventoryOpened = { ...editingInventoryOpened, [pkg.id]: pkg.is_opened };
|
||||
editingInventoryOpened = { ...editingInventoryOpened, [pkg.id]: pkg.is_opened ?? false };
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue