refactor(products): remove obsolete interaction fields across stack

This commit is contained in:
Piotr Oleszczyk 2026-03-04 12:42:12 +01:00
parent 1d8a8eafb8
commit c5ea38880c
16 changed files with 32 additions and 278 deletions

View file

@ -379,16 +379,6 @@
"productForm_activeIrritation": "Irritation",
"productForm_activeFunctions": "Functions",
"productForm_effectProfile": "Effect profile (05)",
"productForm_interactions": "Interactions",
"productForm_synergizesWith": "Synergizes with (one per line)",
"productForm_incompatibleWith": "Incompatible with",
"productForm_addIncompatibility": "+ Add incompatibility",
"productForm_noIncompatibilities": "No incompatibilities added.",
"productForm_incompTarget": "Target ingredient",
"productForm_incompScope": "Scope",
"productForm_incompReason": "Reason (optional)",
"productForm_incompReasonPlaceholder": "e.g. reduces efficacy",
"productForm_incompScopeSelect": "Select…",
"productForm_contextRules": "Context rules",
"productForm_ctxAfterShaving": "Safe after shaving",
"productForm_ctxAfterAcids": "Safe after acids",
@ -495,10 +485,6 @@
"productForm_fnVitaminC": "vitamin C",
"productForm_fnAntiAging": "anti-aging",
"productForm_scopeSameStep": "same step",
"productForm_scopeSameDay": "same day",
"productForm_scopeSamePeriod": "same period",
"productForm_strengthLow": "1 Low",
"productForm_strengthMedium": "2 Medium",
"productForm_strengthHigh": "3 High",

View file

@ -393,16 +393,6 @@
"productForm_activeIrritation": "Podrażnienie",
"productForm_activeFunctions": "Funkcje",
"productForm_effectProfile": "Profil działania (05)",
"productForm_interactions": "Interakcje",
"productForm_synergizesWith": "Synergizuje z (jedno na linię)",
"productForm_incompatibleWith": "Niekompatybilny z",
"productForm_addIncompatibility": "+ Dodaj niekompatybilność",
"productForm_noIncompatibilities": "Brak niekompatybilności.",
"productForm_incompTarget": "Składnik docelowy",
"productForm_incompScope": "Zakres",
"productForm_incompReason": "Powód (opcjonalny)",
"productForm_incompReasonPlaceholder": "np. zmniejsza skuteczność",
"productForm_incompScopeSelect": "Wybierz…",
"productForm_contextRules": "Reguły kontekstu",
"productForm_ctxAfterShaving": "Bezpieczny po goleniu",
"productForm_ctxAfterAcids": "Bezpieczny po kwasach",
@ -509,10 +499,6 @@
"productForm_fnVitaminC": "witamina C",
"productForm_fnAntiAging": "przeciwstarzeniowy",
"productForm_scopeSameStep": "ten sam krok",
"productForm_scopeSameDay": "ten sam dzień",
"productForm_scopeSamePeriod": "ten sam okres",
"productForm_strengthLow": "1 Niskie",
"productForm_strengthMedium": "2 Średnie",
"productForm_strengthHigh": "3 Wysokie",

View file

@ -11,7 +11,6 @@ import type {
Product,
ProductContext,
ProductEffectProfile,
ProductInteraction,
ProductInventory,
Routine,
RoutineSuggestion,
@ -127,8 +126,6 @@ export interface ProductParseResponse {
product_effect_profile?: ProductEffectProfile;
ph_min?: number;
ph_max?: number;
incompatible_with?: ProductInteraction[];
synergizes_with?: string[];
context_rules?: ProductContext;
min_interval_hours?: number;
max_frequency_per_week?: number;

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { untrack } from 'svelte';
import type { Product } from '$lib/types';
import type { IngredientFunction, InteractionScope } from '$lib/types';
import type { IngredientFunction } from '$lib/types';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
@ -34,7 +34,6 @@
'brightening', 'anti_acne', 'ceramide', 'niacinamide',
'sunscreen', 'peptide', 'hair_growth_stimulant', 'prebiotic', 'vitamin_c', 'anti_aging'
];
const interactionScopes: InteractionScope[] = ['same_step', 'same_day', 'same_period'];
// ── Translated label maps ─────────────────────────────────────────────────
@ -125,12 +124,6 @@
anti_aging: m["productForm_fnAntiAging"]()
});
const scopeLabels = $derived<Record<string, string>>({
same_step: m["productForm_scopeSameStep"](),
same_day: m["productForm_scopeSameDay"](),
same_period: m["productForm_scopeSamePeriod"]()
});
const tristate = $derived([
{ value: '', label: m.common_unknown() },
{ value: 'true', label: m.common_yes() },
@ -177,7 +170,6 @@
let usageNotes = $state(untrack(() => product?.usage_notes ?? ''));
let inciText = $state(untrack(() => product?.inci?.join('\n') ?? ''));
let contraindicationsText = $state(untrack(() => product?.contraindications?.join('\n') ?? ''));
let synergizesWithText = $state(untrack(() => product?.synergizes_with?.join('\n') ?? ''));
let recommendedFor = $state<string[]>(untrack(() => [...(product?.recommended_for ?? [])]));
let targetConcerns = $state<string[]>(untrack(() => [...(product?.targets ?? [])]));
@ -239,7 +231,6 @@
if (r.is_tool != null) isTool = r.is_tool;
if (r.inci?.length) inciText = r.inci.join('\n');
if (r.contraindications?.length) contraindicationsText = r.contraindications.join('\n');
if (r.synergizes_with?.length) synergizesWithText = r.synergizes_with.join('\n');
if (r.actives?.length) {
actives = r.actives.map((a) => ({
name: a.name,
@ -249,13 +240,6 @@
irritation_potential: a.irritation_potential != null ? String(a.irritation_potential) : ''
}));
}
if (r.incompatible_with?.length) {
incompatibleWith = r.incompatible_with.map((i) => ({
target: i.target,
scope: i.scope,
reason: i.reason ?? ''
}));
}
if (r.product_effect_profile) {
effectValues = { ...effectValues, ...r.product_effect_profile };
}
@ -380,40 +364,6 @@
)
);
// ── Dynamic incompatible_with ─────────────────────────────────────────────
type IncompatibleRow = { target: string; scope: string; reason: string };
let incompatibleWith: IncompatibleRow[] = $state(
untrack(() =>
product?.incompatible_with?.map((i) => ({
target: i.target,
scope: i.scope,
reason: i.reason ?? ''
})) ?? []
)
);
function addIncompatible() {
incompatibleWith = [...incompatibleWith, { target: '', scope: '', reason: '' }];
}
function removeIncompatible(i: number) {
incompatibleWith = incompatibleWith.filter((_, idx) => idx !== i);
}
let incompatibleJson = $derived(
JSON.stringify(
incompatibleWith
.filter((i) => i.target.trim() && i.scope)
.map((i) => ({
target: i.target.trim(),
scope: i.scope,
...(i.reason.trim() ? { reason: i.reason.trim() } : {})
}))
)
);
const textareaClass =
'w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2';
@ -749,68 +699,6 @@
</CardContent>
</Card>
<!-- ── Interactions ───────────────────────────────────────────────────────── -->
<Card>
<CardHeader><CardTitle>{m["productForm_interactions"]()}</CardTitle></CardHeader>
<CardContent class="space-y-4">
<div class="space-y-2">
<Label for="synergizes_with">{m["productForm_synergizesWith"]()}</Label>
<textarea
id="synergizes_with"
name="synergizes_with"
rows="3"
placeholder="Ceramides&#10;Niacinamide&#10;Retinoids"
class={textareaClass}
bind:value={synergizesWithText}
></textarea>
</div>
<div class="space-y-3">
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<Label>{m["productForm_incompatibleWith"]()}</Label>
<Button type="button" variant="outline" size="sm" onclick={addIncompatible}>
{m["productForm_addIncompatibility"]()}
</Button>
</div>
<input type="hidden" name="incompatible_with_json" value={incompatibleJson} />
{#each incompatibleWith as row, i}
<div class="grid grid-cols-2 gap-2 items-end sm:grid-cols-[1fr_140px_1fr_auto]">
<div class="space-y-1">
<Label class="text-xs">{m["productForm_incompTarget"]()}</Label>
<Input placeholder="e.g. Vitamin C" bind:value={row.target} />
</div>
<div class="space-y-1">
<Label class="text-xs">{m["productForm_incompScope"]()}</Label>
<select class={selectClass} bind:value={row.scope}>
<option value="">{m["productForm_incompScopeSelect"]()}</option>
{#each interactionScopes as s}
<option value={s}>{scopeLabels[s]}</option>
{/each}
</select>
</div>
<div class="space-y-1">
<Label class="text-xs">{m["productForm_incompReason"]()}</Label>
<Input placeholder={m["productForm_incompReasonPlaceholder"]()} bind:value={row.reason} />
</div>
<Button
type="button"
variant="ghost"
size="sm"
onclick={() => removeIncompatible(i)}
class="text-destructive hover:text-destructive"
>✕</Button>
</div>
{/each}
{#if incompatibleWith.length === 0}
<p class="text-sm text-muted-foreground">{m["productForm_noIncompatibilities"]()}</p>
{/if}
</div>
</CardContent>
</Card>
<!-- ── Context rules ──────────────────────────────────────────────────────── -->
<Card>
<CardHeader><CardTitle>{m["productForm_contextRules"]()}</CardTitle></CardHeader>

View file

@ -33,7 +33,6 @@ export type IngredientFunction =
| "prebiotic"
| "vitamin_c"
| "anti_aging";
export type InteractionScope = "same_step" | "same_day" | "same_period";
export type MedicationKind =
| "prescription"
| "otc"
@ -113,12 +112,6 @@ export interface ProductEffectProfile {
anti_aging_strength: number;
}
export interface ProductInteraction {
target: string;
scope: InteractionScope;
reason?: string;
}
export interface ProductContext {
safe_after_shaving?: boolean;
safe_after_acids?: boolean;
@ -172,8 +165,6 @@ export interface Product {
product_effect_profile: ProductEffectProfile;
ph_min?: number;
ph_max?: number;
incompatible_with?: ProductInteraction[];
synergizes_with?: string[];
context_rules?: ProductContext;
min_interval_hours?: number;
max_frequency_per_week?: number;

View file

@ -166,23 +166,6 @@ export const actions: Actions = {
body.actives = null;
}
// Incompatible with
try {
const raw = form.get('incompatible_with_json') as string | null;
if (raw) {
const parsed = JSON.parse(raw);
body.incompatible_with = Array.isArray(parsed) && parsed.length > 0 ? parsed : null;
} else {
body.incompatible_with = null;
}
} catch {
body.incompatible_with = null;
}
// Synergizes with
const synergizes = parseTextList(form.get('synergizes_with') as string | null);
body.synergizes_with = synergizes.length > 0 ? synergizes : null;
// Context rules
body.context_rules = parseContextRules(form) ?? null;

View file

@ -167,19 +167,6 @@ export const actions: Actions = {
}
} catch { /* ignore malformed JSON */ }
// Incompatible with (JSON array)
try {
const raw = form.get('incompatible_with_json') as string | null;
if (raw) {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed) && parsed.length > 0) payload.incompatible_with = parsed;
}
} catch { /* ignore */ }
// Synergizes with
const synergizes = parseTextList(form.get('synergizes_with') as string | null);
if (synergizes.length > 0) payload.synergizes_with = synergizes;
// Context rules
const contextRules = parseContextRules(form);
if (contextRules) payload.context_rules = contextRules;