12 Commits

Author SHA1 Message Date
Your Name
9a30857280 style: hide ID column from print view
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 10:58:33 +03:00
Your Name
f750ef3a01 style: convert mobile column widths to % and font-sizes to rem
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:59:30 +03:00
Your Name
8e3cce0bb0 style: set table cell padding to 1px on mobile
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:17:28 +03:00
Your Name
44961e3a9c style: reduce table cell padding to 1px 2px on mobile
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:15:26 +03:00
Your Name
1cadd41d26 style: wrap table headers at ~5 chars on mobile with word-break break-all
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:14:18 +03:00
Your Name
d53b8b60c9 style: prioritize Название and Расположение columns on mobile
- Инв. номер: 58px -> 44px, Аудитория: 52px -> 34px (both compressed)
- Название: auto (byAud) / auto (allEquipment) — gets most space
- Расположение: shares auto space in allEquipment, 75px in byAud
- Table cell padding: 3px 2px -> 2px 2px

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:12:49 +03:00
Your Name
fc82b38c1f feat: add Тип column to allEquipment table; compress Владелец on mobile
- Add Тип (equipment type) column between Кол-во and Владелец
- allequip-table: table-layout fixed with explicit column widths
- Владелец: 60px (compressed), Название gets remaining space

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:09:02 +03:00
Your Name
a315b9033a style: reduce font size on mobile (body 13px, tables 11px)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:04:41 +03:00
Your Name
8715d21ea3 fix: fix byAud table column widths on mobile with table-layout fixed
- table-layout: fixed so column widths are respected
- Владелец: 88px (compressed, select+button fit inside)
- Название: gets remaining space (widest column)
- Расположение: 80px
- ID/Кол-во/Тип: minimal widths

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 15:02:09 +03:00
Your Name
ec6088904c fix: reduce card-body padding and compress owner column on mobile
- card-body padding: 16px -> 8px on mobile
- owner-td: max-width 110px, select+button stack vertically, font 11px
- Remove d-inline w-auto from owner select (was preventing compression)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 14:56:58 +03:00
Your Name
062d6de913 fix: move inspection action buttons above barcode input; split checked_at into date/time rows
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 14:53:00 +03:00
Your Name
73ce68b071 feat: adapt all pages for mobile (Galaxy S10, 360px)
- Remove min-width: 580px from body — was forcing horizontal scroll
- Reduce container padding to px-2 on mobile
- Fix .inv width (was 400px fixed, now auto)
- Add @media (max-width: 575.98px) block: smaller card margins,
  font sizes, full-width forms and select bars
- Audit/owner/admin/inspection forms: col-auto → col-12 col-sm-auto
- Inspection stats: col-md-3 → col-6 col-md-3 (2x2 grid on mobile)
- Inspection action buttons: d-flex flex-wrap gap-2
- Home search input: flex-grow-1 to fill available width
- aud-select-bar: stacks vertically on mobile
- Bump cache-busting version for styles.css and app.js

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 14:48:56 +03:00
3 changed files with 204 additions and 53 deletions

View File

@@ -312,6 +312,11 @@ createApp({
const d = new Date(dateStr); const d = new Date(dateStr);
return d.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }); return d.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
}, },
formatDateOnly(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr);
return d.toLocaleDateString('ru-RU');
},
async loadEquipmentTypes() { async loadEquipmentTypes() {
try { try {
this.equipmentTypes = await fetchJSON('/equipment-types/'); this.equipmentTypes = await fetchJSON('/equipment-types/');

View File

@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>АСУ Инвентаризация</title> <title>АСУ Инвентаризация</title>
<link rel="stylesheet" href="/app/bootstrap.min.css" /> <link rel="stylesheet" href="/app/bootstrap.min.css" />
<link rel="stylesheet" href="/app/styles.css" /> <link rel="stylesheet" href="/app/styles.css?v=11" />
</head> </head>
<body> <body>
<header class="no-print"> <header class="no-print">
@@ -63,7 +63,7 @@
</nav> </nav>
</div> </div>
<div class="container-fluid px-4"> <div class="container-fluid px-2 px-md-4">
<!-- Главная страница --> <!-- Главная страница -->
<div v-if="view==='home'"> <div v-if="view==='home'">
@@ -72,7 +72,7 @@
<div class="card-body"> <div class="card-body">
<form @submit.prevent="doHomeSearch"> <form @submit.prevent="doHomeSearch">
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
<input type="text" class="form-control w-auto" v-model="homeSearch" placeholder="инвентарный номер" /> <input type="text" class="form-control flex-grow-1" v-model="homeSearch" placeholder="инвентарный номер" />
<button class="btn btn-primary" type="submit">Найти</button> <button class="btn btn-primary" type="submit">Найти</button>
</div> </div>
</form> </form>
@@ -163,9 +163,9 @@
<div class="card-body"> <div class="card-body">
<h3 class="card-title no-print">Оборудование по аудитории</h3> <h3 class="card-title no-print">Оборудование по аудитории</h3>
<h2 class="print-only print-title">{{ printTitle }}</h2> <h2 class="print-only print-title">{{ printTitle }}</h2>
<div class="mb-2 d-flex align-items-center justify-content-center gap-2 no-print"> <div class="mb-2 d-flex align-items-center justify-content-center gap-2 no-print aud-select-bar">
<label for="aud-select" class="mb-0">Аудитория:</label> <label for="aud-select" class="mb-0">Аудитория:</label>
<select id="aud-select" class="form-select w-auto" v-model="selectedAudId"> <select id="aud-select" class="form-select" style="max-width:220px;" v-model="selectedAudId">
<option value="">— выберите аудиторию —</option> <option value="">— выберите аудиторию —</option>
<option v-for="a in auditories" :key="a.id" :value="a.id">{{ a.audnazvanie }}</option> <option v-for="a in auditories" :key="a.id" :value="a.id">{{ a.audnazvanie }}</option>
</select> </select>
@@ -174,10 +174,10 @@
</div> </div>
<div class="status no-print" :class="{error: !!error}">{{ status }}</div> <div class="status no-print" :class="{error: !!error}">{{ status }}</div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table datatable"> <table class="table datatable byaud-table">
<thead> <thead>
<tr> <tr>
<th scope="col">ID</th> <th scope="col" class="no-print">ID</th>
<th scope="col">Инв. номер</th> <th scope="col">Инв. номер</th>
<th scope="col">Название</th> <th scope="col">Название</th>
<th scope="col">Расположение</th> <th scope="col">Расположение</th>
@@ -189,19 +189,19 @@
</thead> </thead>
<tbody> <tbody>
<tr v-for="it in oboruds" :key="it.id"> <tr v-for="it in oboruds" :key="it.id">
<td>{{ it.id }}</td> <td class="no-print">{{ it.id }}</td>
<td class="inv">{{ it.invNumber ?? '' }}</td> <td class="inv">{{ it.invNumber ?? '' }}</td>
<td>{{ it.nazvanie ?? '' }}</td> <td>{{ it.nazvanie ?? '' }}</td>
<td class="rasp">{{ it.raspologenie ?? '' }}</td> <td class="rasp">{{ it.raspologenie ?? '' }}</td>
<td class="no-print">{{ it.kolichestvo ?? '' }}</td> <td class="no-print">{{ it.kolichestvo ?? '' }}</td>
<td class="no-print">{{ it.type?.name ?? '' }}</td> <td class="no-print">{{ it.type?.name ?? '' }}</td>
<td class="no-print"> <td class="no-print owner-td">
<template v-if="canEdit"> <template v-if="canEdit">
<select class="form-select form-select-sm d-inline w-auto" v-model="it.selectedOwnerId"> <select class="form-select form-select-sm" v-model="it.selectedOwnerId">
<option value="">— нет —</option> <option value="">— нет —</option>
<option v-for="ow in owners" :key="ow.id" :value="ow.id">{{ ow.name }}</option> <option v-for="ow in owners" :key="ow.id" :value="ow.id">{{ ow.name }}</option>
</select> </select>
<button class="btn btn-sm btn-outline-primary ms-2" @click="saveOwner(it)">Сохранить</button> <button class="btn btn-sm btn-outline-primary" @click="saveOwner(it)">Сохранить</button>
</template> </template>
<template v-else> <template v-else>
{{ it.owner?.name ?? '' }} {{ it.owner?.name ?? '' }}
@@ -226,7 +226,7 @@
</div> </div>
<div class="status no-print" :class="{error: !!error}">{{ status }}</div> <div class="status no-print" :class="{error: !!error}">{{ status }}</div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table datatable"> <table class="table datatable allequip-table">
<thead> <thead>
<tr> <tr>
<th scope="col" class="num-col"></th> <th scope="col" class="num-col"></th>
@@ -235,6 +235,7 @@
<th scope="col" class="aud-col">Аудитория</th> <th scope="col" class="aud-col">Аудитория</th>
<th scope="col">Расположение</th> <th scope="col">Расположение</th>
<th scope="col" class="no-print">Кол-во</th> <th scope="col" class="no-print">Кол-во</th>
<th scope="col" class="no-print">Тип</th>
<th scope="col" class="no-print">Владелец</th> <th scope="col" class="no-print">Владелец</th>
</tr> </tr>
</thead> </thead>
@@ -246,6 +247,7 @@
<td class="aud-col">{{ getAuditoryName(it.aud_id) }}</td> <td class="aud-col">{{ getAuditoryName(it.aud_id) }}</td>
<td class="rasp">{{ it.raspologenie ?? '' }}</td> <td class="rasp">{{ it.raspologenie ?? '' }}</td>
<td class="no-print">{{ it.kolichestvo ?? '' }}</td> <td class="no-print">{{ it.kolichestvo ?? '' }}</td>
<td class="no-print">{{ it.type?.name ?? '' }}</td>
<td class="no-print">{{ it.owner?.name ?? '' }}</td> <td class="no-print">{{ it.owner?.name ?? '' }}</td>
</tr> </tr>
</tbody> </tbody>
@@ -264,16 +266,16 @@
<div v-else> <div v-else>
<h5 class="mt-2">Создать пользователя-админа</h5> <h5 class="mt-2">Создать пользователя-админа</h5>
<div class="row g-2 align-items-end"> <div class="row g-2 align-items-end">
<div class="col-auto"> <div class="col-12 col-sm-auto">
<label class="form-label">Логин</label> <label class="form-label">Логин</label>
<input class="form-control" v-model="newAdminUsername" placeholder="username" /> <input class="form-control" v-model="newAdminUsername" placeholder="username" />
</div> </div>
<div class="col-auto"> <div class="col-12 col-sm-auto">
<label class="form-label">Пароль</label> <label class="form-label">Пароль</label>
<input type="password" class="form-control" v-model="newAdminPassword" placeholder="password" /> <input type="password" class="form-control" v-model="newAdminPassword" placeholder="password" />
</div> </div>
<div class="col-auto"> <div class="col-12 col-sm-auto">
<button class="btn btn-success" @click="createAdmin">Создать админа</button> <button class="btn btn-success w-100" @click="createAdmin">Создать админа</button>
</div> </div>
</div> </div>
<div class="status mt-2" :class="{error: !!error}">{{ status }}</div> <div class="status mt-2" :class="{error: !!error}">{{ status }}</div>
@@ -311,12 +313,12 @@
<div v-else> <div v-else>
<h5 class="mt-2">Добавить аудиторию</h5> <h5 class="mt-2">Добавить аудиторию</h5>
<div class="row g-2 align-items-end"> <div class="row g-2 align-items-end">
<div class="col-auto"> <div class="col-12 col-sm-auto">
<label class="form-label">Название аудитории</label> <label class="form-label">Название аудитории</label>
<input class="form-control" v-model="newAudName" placeholder="например, 519" /> <input class="form-control" v-model="newAudName" placeholder="например, 519" />
</div> </div>
<div class="col-auto"> <div class="col-12 col-sm-auto">
<button class="btn btn-success" @click="createAuditory">Добавить</button> <button class="btn btn-success w-100" @click="createAuditory">Добавить</button>
</div> </div>
</div> </div>
<div class="status mt-2" :class="{error: !!error}">{{ status }}</div> <div class="status mt-2" :class="{error: !!error}">{{ status }}</div>
@@ -351,12 +353,12 @@
<div v-else> <div v-else>
<h5 class="mt-2">Добавить владельца</h5> <h5 class="mt-2">Добавить владельца</h5>
<div class="row g-2 align-items-end"> <div class="row g-2 align-items-end">
<div class="col-auto"> <div class="col-12 col-sm-auto">
<label class="form-label">Имя владельца</label> <label class="form-label">Имя владельца</label>
<input class="form-control" v-model="newOwnerName" placeholder="например, Иванов И.И." /> <input class="form-control" v-model="newOwnerName" placeholder="например, Иванов И.И." />
</div> </div>
<div class="col-auto"> <div class="col-12 col-sm-auto">
<button class="btn btn-success" @click="createOwner">Добавить</button> <button class="btn btn-success w-100" @click="createOwner">Добавить</button>
</div> </div>
</div> </div>
<div class="status mt-2" :class="{error: !!error}">{{ status }}</div> <div class="status mt-2" :class="{error: !!error}">{{ status }}</div>
@@ -580,15 +582,15 @@
<div v-if="!activeInspection"> <div v-if="!activeInspection">
<h5>Начать новую проверку</h5> <h5>Начать новую проверку</h5>
<div class="row g-2 align-items-end mb-3"> <div class="row g-2 align-items-end mb-3">
<div class="col-auto"> <div class="col-12 col-sm-auto">
<label class="form-label">Аудитория (необязательно)</label> <label class="form-label">Аудитория (необязательно)</label>
<select class="form-select" v-model="inspectionAudId"> <select class="form-select" v-model="inspectionAudId">
<option value="">— Всё оборудование —</option> <option value="">— Всё оборудование —</option>
<option v-for="a in auditories" :key="a.id" :value="a.id">{{ a.audnazvanie }}</option> <option v-for="a in auditories" :key="a.id" :value="a.id">{{ a.audnazvanie }}</option>
</select> </select>
</div> </div>
<div class="col-auto"> <div class="col-12 col-sm-auto">
<button class="btn btn-success" @click="startInspection">Начать проверку</button> <button class="btn btn-success w-100" @click="startInspection">Начать проверку</button>
</div> </div>
</div> </div>
@@ -631,41 +633,48 @@
</div> </div>
<!-- Статистика прогресса --> <!-- Статистика прогресса -->
<div class="row mb-3"> <div class="row mb-3 g-2">
<div class="col-md-3"> <div class="col-6 col-md-3">
<div class="card text-center"> <div class="card text-center h-100">
<div class="card-body"> <div class="card-body py-2">
<h5 class="card-title">{{ inspectionStats.total_checked }}</h5> <h5 class="card-title">{{ inspectionStats.total_checked }}</h5>
<p class="card-text">Проверено</p> <p class="card-text small">Проверено</p>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-6 col-md-3">
<div class="card text-center"> <div class="card text-center h-100">
<div class="card-body"> <div class="card-body py-2">
<h5 class="card-title">{{ inspectionStats.total_expected }}</h5> <h5 class="card-title">{{ inspectionStats.total_expected }}</h5>
<p class="card-text">Всего</p> <p class="card-text small">Всего</p>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-6 col-md-3">
<div class="card text-center"> <div class="card text-center h-100">
<div class="card-body"> <div class="card-body py-2">
<h5 class="card-title">{{ inspectionStats.total_unknown }}</h5> <h5 class="card-title">{{ inspectionStats.total_unknown }}</h5>
<p class="card-text">Не найдено</p> <p class="card-text small">Не найдено</p>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-6 col-md-3">
<div class="card text-center bg-success text-white"> <div class="card text-center h-100 bg-success text-white">
<div class="card-body"> <div class="card-body py-2">
<h5 class="card-title">{{ inspectionStats.progress_percent }}%</h5> <h5 class="card-title">{{ inspectionStats.progress_percent }}%</h5>
<p class="card-text">Прогресс</p> <p class="card-text small">Прогресс</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Кнопки действий -->
<div class="d-flex flex-wrap gap-2 mb-3">
<button class="btn btn-primary" @click="refreshInspectionData">Обновить данные</button>
<button class="btn btn-success" @click="completeInspection">Завершить проверку</button>
<button class="btn btn-secondary" @click="cancelInspection">Отменить</button>
</div>
<!-- Поле для сканирования --> <!-- Поле для сканирования -->
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Сканируйте штрихкод или введите инв. номер</label> <label class="form-label">Сканируйте штрихкод или введите инв. номер</label>
@@ -688,13 +697,6 @@
</div> </div>
</div> </div>
<!-- Кнопки действий -->
<div class="mt-3 mb-4">
<button class="btn btn-primary me-2" @click="refreshInspectionData">Обновить данные</button>
<button class="btn btn-success me-2" @click="completeInspection">Завершить проверку</button>
<button class="btn btn-secondary" @click="cancelInspection">Отменить</button>
</div>
<!-- Таблица проверенного оборудования (в реальном времени) --> <!-- Таблица проверенного оборудования (в реальном времени) -->
<h5 class="mt-4">Проверенное оборудование</h5> <h5 class="mt-4">Проверенное оборудование</h5>
<div class="table-responsive"> <div class="table-responsive">
@@ -712,7 +714,7 @@
<td>{{ rec.oborud?.invNumber }}</td> <td>{{ rec.oborud?.invNumber }}</td>
<td>{{ rec.oborud?.nazvanie }}</td> <td>{{ rec.oborud?.nazvanie }}</td>
<td>{{ getAuditoryName(rec.oborud?.aud_id) }}</td> <td>{{ getAuditoryName(rec.oborud?.aud_id) }}</td>
<td>{{ formatDate(rec.checked_at) }}</td> <td><div>{{ formatDateOnly(rec.checked_at) }}</div><div>{{ formatTime(rec.checked_at) }}</div></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -735,6 +737,6 @@
<script src="/app/vue.global.prod.js"></script> <script src="/app/vue.global.prod.js"></script>
<script src="/app/bootstrap.bundle.min.js"></script> <script src="/app/bootstrap.bundle.min.js"></script>
<script src="/app/app.js?v=2" defer></script> <script src="/app/app.js?v=4" defer></script>
</body> </body>
</html> </html>

View File

@@ -5,7 +5,7 @@
body { body {
background-color: #E2F3FD; background-color: #E2F3FD;
min-width: 580px; min-width: 0;
} }
.row { .row {
@@ -125,7 +125,8 @@ td.inv-col, th.inv-col {
} }
.inv { .inv {
width: 400px; width: auto;
min-width: 80px;
} }
.rasp { .rasp {
@@ -158,6 +159,149 @@ td.inv-col, th.inv-col {
width: 200px; width: 200px;
} }
@media (max-width: 575.98px) {
header h1 {
font-size: 1.2rem;
}
.container-fluid {
padding-left: 8px !important;
padding-right: 8px !important;
}
.card {
margin: 4px;
border-radius: 10px;
}
.rasp {
width: auto;
max-width: 120px;
}
body {
font-size: 0.8rem;
}
.card-body {
padding: 8px;
}
.table td, .table th {
font-size: 0.69rem;
padding: 1px;
}
.byaud-table th,
.allequip-table th {
white-space: normal;
word-break: break-all;
line-height: 1.2;
}
/* byAud table: Название и Расположение — приоритет */
.byaud-table {
table-layout: fixed;
width: 100%;
}
/* ID */
.byaud-table th:nth-child(1),
.byaud-table td:nth-child(1) { width: 6%; }
/* Инв. номер */
.byaud-table th:nth-child(2),
.byaud-table td:nth-child(2) { width: 12%; }
/* Название — auto, gets most remaining space */
/* Расположение */
.byaud-table th:nth-child(4),
.byaud-table td:nth-child(4) { width: 20%; }
/* Кол-во */
.byaud-table th:nth-child(5),
.byaud-table td:nth-child(5) { width: 7%; }
/* Тип */
.byaud-table th:nth-child(6),
.byaud-table td:nth-child(6) { width: 12%; }
/* Владелец */
.byaud-table th:nth-child(7),
.byaud-table td:nth-child(7) { width: 25%; }
/* allEquipment table: Название и Расположение — приоритет */
.allequip-table {
table-layout: fixed;
width: 100%;
}
/* № */
.allequip-table th:nth-child(1),
.allequip-table td:nth-child(1) { width: 5%; }
/* Инв. номер */
.allequip-table th:nth-child(2),
.allequip-table td:nth-child(2) { width: 12%; }
/* Название — auto, gets most space */
/* Аудитория */
.allequip-table th:nth-child(4),
.allequip-table td:nth-child(4) { width: 9%; }
/* Расположение — auto, shares space with Название */
/* Кол-во */
.allequip-table th:nth-child(6),
.allequip-table td:nth-child(6) { width: 7%; }
/* Тип */
.allequip-table th:nth-child(7),
.allequip-table td:nth-child(7) { width: 13%; }
/* Владелец */
.allequip-table th:nth-child(8),
.allequip-table td:nth-child(8) { width: 15%; }
.owner-td select {
width: 100% !important;
max-width: 100%;
margin-bottom: 3px;
font-size: 0.69rem;
}
.owner-td .btn {
width: 100%;
padding: 2px 4px;
font-size: 0.69rem;
margin: 0 !important;
}
.aud-select-bar {
flex-direction: column !important;
align-items: stretch !important;
justify-content: flex-start !important;
}
.aud-select-bar label {
margin-bottom: 4px;
}
.aud-select-bar select,
.aud-select-bar .btn {
width: 100% !important;
max-width: 100% !important;
}
.form-row-mobile {
flex-direction: column !important;
}
.form-row-mobile .col-auto,
.form-row-mobile [class*="col-"] {
width: 100% !important;
max-width: 100% !important;
}
.btn-bar {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.btn-bar .btn {
flex: 1 1 auto;
margin: 0 !important;
}
}
@media print { @media print {
* { * {
font-family: "Times New Roman", Times, serif; font-family: "Times New Roman", Times, serif;