diff --git a/mechanic-pwa/frontend/src/api/warehouse.ts b/mechanic-pwa/frontend/src/api/warehouse.ts
index 0575716..c5d8197 100644
--- a/mechanic-pwa/frontend/src/api/warehouse.ts
+++ b/mechanic-pwa/frontend/src/api/warehouse.ts
@@ -379,6 +379,18 @@ export interface TyreStockRow {
fits: boolean;
}
+/** Докладка в выданный комплект — ещё колесо на ту же машину. */
+export interface TyreAddition {
+ id: number;
+ part_id: number;
+ part_name: string | null;
+ qty: number;
+ grade: TyreGrade | null;
+ created_at: string;
+ created_by: string | null;
+ comment: string | null;
+}
+
export interface TyreSet {
id: number;
car_id: number | null;
@@ -400,6 +412,8 @@ export interface TyreSet {
grade_in: TyreGrade | null;
close_comment: string | null;
comment: string | null;
+ /** Докладки. `qty` выше их уже включает: это все колёса на машине. */
+ additions?: TyreAddition[];
}
export interface TyreContext {
@@ -437,6 +451,8 @@ export async function issueTyres(
place_id?: number | null;
comment?: string | null;
close?: TyreClose | null;
+ /** Доложить в открытый комплект вместо выдачи нового (тогда close не нужен). */
+ add_to_open?: boolean;
},
idempotencyKey: string,
): Promise {
diff --git a/mechanic-pwa/frontend/src/pages/warehouse/TyresPage.tsx b/mechanic-pwa/frontend/src/pages/warehouse/TyresPage.tsx
index 492b6d2..b05dc92 100644
--- a/mechanic-pwa/frontend/src/pages/warehouse/TyresPage.tsx
+++ b/mechanic-pwa/frontend/src/pages/warehouse/TyresPage.tsx
@@ -53,9 +53,11 @@ function daysOn(iso: string): number {
/** Подпись кнопки выдачи. «Комплект» — только когда колёс действительно четыре:
* с тех пор как выдавать можно поштучно, на одной покрышке эта подпись врала. */
-function issueLabel(qty: number): string {
+function issueLabel(qty: number, addToOpen: boolean): string {
+ const noun = qty === 1 ? "покрышку" : "покрышки";
+ if (addToOpen) return `Доложить ${qty} ${noun}`;
if (qty === 4) return "Выдать комплект";
- return `Выдать ${qty} ${qty === 1 ? "покрышку" : "покрышки"}`;
+ return `Выдать ${qty} ${noun}`;
}
export default function TyresPage() {
@@ -179,6 +181,7 @@ function IssueForm({ carId, onDone }: { carId: number; onDone: () => void }) {
const [comment, setComment] = useState("");
const [saving, setSaving] = useState(false);
const [showAll, setShowAll] = useState(false);
+ const [addToOpen, setAddToOpen] = useState(false);
// Ключ идемпотентности живёт, пока форма открыта: ретрай после обрыва связи
// должен попасть в ТУ ЖЕ выдачу, а не создать вторую.
const [idemKey] = useState(() => crypto.randomUUID());
@@ -208,7 +211,10 @@ function IssueForm({ carId, onDone }: { carId: number; onDone: () => void }) {
}
const openSet = ctx.open_set;
- const needClose = openSet !== null && closeAction === null;
+ // «Доложить» — когда старые колёса остаются на машине, а добавляется ещё
+ // одно-два. Закрывать комплект ради этого нельзя: «списали» врёт на живой
+ // резине, «вернули» приходует на склад то, что с машины не снимали.
+ const needClose = openSet !== null && !addToOpen && closeAction === null;
const chosen = ctx.stock.find((s) => s.part_id === partId) ?? null;
const submit = async () => {
@@ -225,16 +231,20 @@ function IssueForm({ carId, onDone }: { carId: number; onDone: () => void }) {
qty,
grade,
comment: comment.trim() || null,
- close: closeAction
- ? {
- action: closeAction,
- grade: closeAction === "returned" ? closeGrade : undefined,
- }
- : null,
+ add_to_open: addToOpen,
+ close:
+ !addToOpen && closeAction
+ ? {
+ action: closeAction,
+ grade: closeAction === "returned" ? closeGrade : undefined,
+ }
+ : null,
},
idemKey,
);
- toast.success(`Выдано ${qty} шт · ${ctx.car_number ?? ""}`.trim());
+ toast.success(
+ `${addToOpen ? "Доложено" : "Выдано"} ${qty} шт · ${ctx.car_number ?? ""}`.trim(),
+ );
onDone();
} catch (e) {
toast.error(await apiErrorText(e, "Не удалось выдать резину"));
@@ -275,20 +285,55 @@ function IssueForm({ carId, onDone }: { carId: number; onDone: () => void }) {
{` (${daysOn(openSet.issued_at)} дн.)`}
{openSet.grade_out ? ` · при выдаче ${TYRE_GRADE_LABEL[openSet.grade_out].toLowerCase()}` : ""}
- Что с ним?
-
- {(Object.keys(CLOSE_LABEL) as TyreClose["action"][]).map((a) => (
-
- ))}
+ {openSet.additions && openSet.additions.length > 0 && (
+
+ из них доложено:{" "}
+ {openSet.additions
+ .map((a) => `${a.qty} шт ${fmtDate(a.created_at)}`)
+ .join(", ")}
+
+ )}
+
+ {/* Развилка: доложить к тем колёсам или заменить их целиком. */}
+
+
+
- {closeAction === "returned" && (
+
+ {addToOpen ? (
+
+ Комплект остаётся тем же, колёса добавятся к нему. Размер должен совпадать.
+
+ ) : (
+ <>
+
Что со старым?
+
+ {(Object.keys(CLOSE_LABEL) as TyreClose["action"][]).map((a) => (
+
+ ))}
+
+ >
+ )}
+ {!addToOpen && closeAction === "returned" && (
Состояние снятой резины
@@ -383,8 +428,8 @@ function IssueForm({ carId, onDone }: { carId: number; onDone: () => void }) {
{saving
? "Выдаём…"
: needClose
- ? "Сначала закройте предыдущий комплект"
- : issueLabel(qty)}
+ ? "Сначала выберите: доложить или заменить"
+ : issueLabel(qty, addToOpen)}