repairs: несколько замен масла за заезд (двигатель + коробка)

Блок «Замена масла» теперь мульти: чекбоксы Двигатель / Коробка (можно оба),
общий одометр (одна замена — один пробег). Отправляем список oil_changes
(1:N), стикер прикрепляем к первой выбранной. Тип 'transmission' объединяет
АКПП/вариатор/робот — интервал по модели в отчёте СТО.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 13:37:38 +10:00
co-authored by Claude Opus 4.8
parent 3c27b794dd
commit ce4b50109a
2 changed files with 60 additions and 46 deletions
@@ -72,7 +72,8 @@ export interface CreateRepairPayload {
works: CreateWorkPayload[]; works: CreateWorkPayload[];
parts: CreatePartPayload[]; parts: CreatePartPayload[];
photos: CreatePhotoPayload[]; photos: CreatePhotoPayload[];
oil_change: CreateOilChangePayload | null; // 1:N — за один заезд бывает несколько замен (двигатель + коробка).
oil_changes: CreateOilChangePayload[];
} }
export interface CreateRepairResponse { export interface CreateRepairResponse {
@@ -61,14 +61,13 @@ interface PartRow {
qty: number; qty: number;
} }
type OilKind = "engine" | "cvt" | "akpp";
interface OilChangeState { interface OilChangeState {
enabled: boolean; enabled: boolean;
mileage_at_change: number | null; mileage_at_change: number | null; // общий одометр — один на все замены заезда
mileage_is_dirty: boolean; // У24: мягкая подписка с шапочным mileage_is_dirty: boolean; // У24: мягкая подписка с шапочным
next_in_km: number; next_in_km: number; // интервал двигателя
oil_type: OilKind; // Какие масла меняли за заезд (можно оба сразу):
types: { engine: boolean; transmission: boolean };
sticker: PhotoState | null; sticker: PhotoState | null;
} }
@@ -101,7 +100,7 @@ export default function CreateRepairPage() {
mileage_at_change: null, mileage_at_change: null,
mileage_is_dirty: false, mileage_is_dirty: false,
next_in_km: 10000, next_in_km: 10000,
oil_type: "engine", types: { engine: true, transmission: false },
sticker: null, sticker: null,
}); });
@@ -272,6 +271,8 @@ export default function CreateRepairPage() {
if (works.length === 0) return "Добавьте хотя бы одну работу"; if (works.length === 0) return "Добавьте хотя бы одну работу";
const unlockedNoComment = works.find((w) => w.unlocked && !w.override_comment.trim()); const unlockedNoComment = works.find((w) => w.unlocked && !w.override_comment.trim());
if (unlockedNoComment) return "Укажите комментарий к ручной правке цены"; if (unlockedNoComment) return "Укажите комментарий к ручной правке цены";
if (oilChange.enabled && !oilChange.types.engine && !oilChange.types.transmission)
return "Выберите тип масла (двигатель и/или коробка) или снимите «Была замена масла»";
return ""; return "";
}; };
@@ -301,22 +302,25 @@ export default function CreateRepairPage() {
})), })),
parts: parts.map((p) => ({ name: p.name, qty: p.qty })), parts: parts.map((p) => ({ name: p.name, qty: p.qty })),
photos: photoPayload, photos: photoPayload,
oil_change: oilChange.enabled oil_changes: oilChange.enabled
? { ? (["engine", "transmission"] as const)
enabled: true, .filter((t) => oilChange.types[t])
mileage_at_change: oilChange.mileage_at_change, .map((t, idx) => ({
next_in_km: oilChange.next_in_km, enabled: true,
oil_type: oilChange.oil_type, mileage_at_change: oilChange.mileage_at_change,
sticker_photo_uuid: next_in_km: t === "engine" ? oilChange.next_in_km : 0,
oilChange.sticker?.status === "uploaded" oil_type: t,
? oilChange.sticker.presign.photo_uuid // Стикер прикрепляем к первой выбранной замене (обычно двигатель).
: null, sticker_photo_uuid:
sticker_extension: idx === 0 && oilChange.sticker?.status === "uploaded"
oilChange.sticker?.status === "uploaded" ? oilChange.sticker.presign.photo_uuid
? oilChange.sticker.presign.extension : null,
: null, sticker_extension:
} idx === 0 && oilChange.sticker?.status === "uploaded"
: null, ? oilChange.sticker.presign.extension
: null,
}))
: [],
}, },
idemKey.current, idemKey.current,
); );
@@ -585,25 +589,34 @@ export default function CreateRepairPage() {
{oilChange.enabled && ( {oilChange.enabled && (
<div className="mt-3 space-y-3"> <div className="mt-3 space-y-3">
<div> <div>
<Label className="text-sm">Тип масла</Label> <Label className="text-sm">Что меняли (можно оба)</Label>
<div className="mt-1 grid grid-cols-3 gap-2"> <div className="mt-1 grid grid-cols-2 gap-2">
{([["engine", "Двигатель"], ["cvt", "Вариатор"], ["akpp", "АКПП"]] as [OilKind, string][]).map( {(
([v, l]) => ( [
<button ["engine", "Двигатель"],
type="button" ["transmission", "Коробка"],
key={v} ] as ["engine" | "transmission", string][]
onClick={() => setOilChange((oc) => ({ ...oc, oil_type: v }))} ).map(([v, l]) => (
className={`rounded-md border px-2 py-2 text-sm ${ <button
oilChange.oil_type === v type="button"
? "border-primary bg-primary/10 font-medium text-primary" key={v}
: "border-input" onClick={() =>
}`} setOilChange((oc) => ({ ...oc, types: { ...oc.types, [v]: !oc.types[v] } }))
> }
{l} className={`rounded-md border px-2 py-2 text-sm ${
</button> oilChange.types[v]
), ? "border-primary bg-primary/10 font-medium text-primary"
)} : "border-input"
}`}
>
{oilChange.types[v] ? "✓ " : ""}
{l}
</button>
))}
</div> </div>
{!oilChange.types.engine && !oilChange.types.transmission && (
<p className="mt-1 text-xs text-destructive">Выберите хотя бы один тип масла.</p>
)}
</div> </div>
<div> <div>
<Label className="text-sm">Пробег на момент замены</Label> <Label className="text-sm">Пробег на момент замены</Label>
@@ -620,9 +633,9 @@ export default function CreateRepairPage() {
} }
/> />
</div> </div>
{oilChange.oil_type === "engine" ? ( {oilChange.types.engine && (
<div> <div>
<Label className="text-sm">Следующая замена через (км)</Label> <Label className="text-sm">Двигатель: следующая замена через (км)</Label>
<Input <Input
type="number" type="number"
min={0} min={0}
@@ -632,10 +645,10 @@ export default function CreateRepairPage() {
} }
/> />
</div> </div>
) : ( )}
{oilChange.types.transmission && (
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Интервал {oilChange.oil_type === "cvt" ? "вариатора" : "АКПП"} задаётся по модели авто в Интервал коробки задаётся по модели авто в отчёте СТО.
отчёте СТО.
</p> </p>
)} )}
<div> <div>