Initial Commit

This commit is contained in:
2026-04-10 14:48:52 +02:00
commit fad7d9a757
81 changed files with 39657 additions and 0 deletions
+99
View File
@@ -0,0 +1,99 @@
/*
* Copyright 2025-2026 AIIrondev
*
* Licensed under the Inventarsystem EULA (Endbenutzer-Lizenzvertrag).
* See Legal/LICENSE for the full license text.
* Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
* For commercial licensing inquiries: https://github.com/AIIrondev
*/
/* Styles for planned appointments in item details */
.planned-appointments-panel {
margin: 15px 0;
padding: 15px;
background-color: #e1f5fe;
border-left: 4px solid #03a9f4;
border-radius: 4px;
}
.planned-appointments-panel h4 {
color: #0277bd;
margin-top: 0;
margin-bottom: 10px;
font-size: 1.1em;
}
.appointment-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.appointment-item {
padding: 10px;
background-color: #f9fdff;
border-radius: 4px;
border: 1px solid #b3e5fc;
}
.appointment-header {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 5px;
}
.appointment-date {
font-weight: bold;
color: #0288d1;
}
.appointment-period {
background-color: #03a9f4;
color: white;
padding: 2px 8px;
border-radius: 10px;
font-size: 0.8em;
}
.appointment-status {
padding: 2px 8px;
border-radius: 10px;
font-size: 0.8em;
font-weight: bold;
margin-left: 5px;
}
.status-planned {
background-color: #03a9f4;
color: white;
}
.status-active {
background-color: #4CAF50;
color: white;
}
.status-completed {
background-color: #9E9E9E;
color: white;
}
.status-cancelled {
background-color: #F44336;
color: white;
}
.appointment-user {
margin-left: auto;
font-style: italic;
color: #546e7a;
}
.appointment-notes {
font-size: 0.9em;
color: #455a64;
margin-top: 5px;
padding-top: 5px;
border-top: 1px solid #e1f5fe;
font-style: italic;
}
+806
View File
@@ -0,0 +1,806 @@
/*
* Copyright 2025-2026 AIIrondev
*
* Licensed under the Inventarsystem EULA (Endbenutzer-Lizenzvertrag).
* See Legal/LICENSE for the full license text.
* Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
* For commercial licensing inquiries: https://github.com/AIIrondev
*/
/* Global visual system for all pages */
:root {
--ui-bg: #f2f5f8;
--ui-bg-accent: #e7edf4;
--ui-surface: #ffffff;
--ui-surface-soft: #f8fafc;
--ui-border: #d8e0e8;
--ui-text: #1f2d3d;
--ui-text-muted: #5d6d7e;
--ui-title: #1a2e44;
--ui-shadow-sm: 0 2px 8px rgba(15, 23, 42, 0.08);
--ui-shadow-md: 0 10px 24px rgba(15, 23, 42, 0.1);
--ui-radius: 12px;
}
body {
font-family: "Manrope", "Segoe UI", "Noto Sans", sans-serif;
color: var(--ui-text);
background: var(--ui-bg);
margin: 0;
padding: 0;
}
.container {
background-color: var(--ui-surface);
border: 0;
padding: 22px;
border-radius: var(--ui-radius);
box-shadow: none;
width: min(1200px, calc(100% - 28px));
margin: 16px auto;
}
.container-fluid {
padding-left: 16px;
padding-right: 16px;
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: var(--ui-title);
letter-spacing: 0.01em;
}
h1,
h2 {
text-align: center;
}
p,
small,
label,
span {
color: var(--ui-text-muted);
}
.card,
.content,
.form-card,
.modal-content,
.list-group-item,
.dropdown-menu,
.table,
.alert {
border-radius: var(--ui-radius);
}
.card,
.content,
.form-card,
.modal-content,
.list-group-item,
.dropdown-menu,
.table {
border: 0;
box-shadow: none;
background: var(--ui-surface);
}
.card-header,
.modal-header,
thead th {
background: linear-gradient(180deg, #f7fafd, #edf3f8) !important;
border-bottom: 1px solid var(--ui-border) !important;
color: var(--ui-title) !important;
}
.form-control,
.form-select,
textarea,
input[type="text"],
input[type="password"],
input[type="number"],
input[type="email"],
input[type="date"],
select {
border-radius: 10px !important;
border: 1px solid #cad5e0 !important;
min-height: 40px;
color: var(--ui-text) !important;
background-color: #fff !important;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.form-control:focus,
.form-select:focus,
textarea:focus,
input:focus,
select:focus {
border-color: #7ea3c8 !important;
box-shadow: 0 0 0 0.2rem rgba(70, 130, 180, 0.14) !important;
outline: 0;
}
.table {
overflow: hidden;
}
.table th,
.table td {
vertical-align: middle;
border-color: #dee6ee !important;
}
.table-striped > tbody > tr:nth-of-type(odd) {
background-color: #f8fbff;
}
.alert {
border-width: 1px;
box-shadow: none;
}
.navbar {
border-bottom: 1px solid #273341;
box-shadow: none;
}
.navbar-brand {
font-weight: 800;
letter-spacing: 0.02em;
}
@media (max-width: 900px) {
.container {
width: calc(100% - 18px);
padding: 14px;
margin: 10px auto;
border-radius: 10px;
}
}
/* Edit and generate buttons with black text for better visibility */
.edit-button {
color: black !important; /* Override any existing color */
}
.duplicate-button {
color: black !important; /* Override any existing color */
}
.generate-qr-button {
color: black !important; /* Override any existing color */
}
.ausleihen {
color: black !important; /* Override any existing color */
}
/* Modal image display fix */
.modal-image {
display: none;
max-width: 100%;
max-height: 80vh;
margin: 0 auto;
}
.modal-image.active-image {
display: block;
}
/* Active image display classes */
.item-image.active-image {
display: block !important;
opacity: 1 !important;
z-index: 5;
}
.item-image:not(.active-image) {
display: none !important;
}
/* QR Reader styling */
#qr-reader {
width: 500px;
display: none;
}
/* Edit new location container */
.edit-new-location-container {
display: none;
margin-top: 10px;
}
/* Modal dialog styling */
.modal-dialog-white {
background: white;
padding: 20px;
border-radius: 5px;
text-align: center;
}
.modal-content-margin {
margin-top: 10px;
}
/* Unified list/card view switch styling for main and main_admin */
.view-switch-group {
position: absolute;
right: 0;
top: 0;
display: flex;
gap: 8px;
padding: 4px;
border-radius: 999px;
background: transparent;
border: 0;
box-shadow: none;
}
.view-toggle-btn {
width: 38px;
height: 38px;
border: 1px solid #c7ced6;
border-radius: 50%;
background: #ffffff;
font-size: 1.15rem;
cursor: pointer;
color: #2f3e4e;
display: inline-flex;
align-items: center;
justify-content: center;
transition: transform 0.12s ease, box-shadow 0.18s ease, border-color 0.18s ease, background-color 0.18s ease;
}
.view-toggle-btn:hover {
transform: translateY(-1px);
border-color: #8fa2b7;
background: #f8fbff;
box-shadow: 0 3px 8px rgba(33, 37, 41, 0.12);
}
.view-toggle-btn:active {
transform: translateY(0);
}
.view-toggle-btn[aria-pressed='true'] {
background: #e9f2ff;
border-color: #7ea1c8;
color: #1f4f7a;
}
.table-view-header {
display: none;
}
body.table-view .table-view-header {
display: grid;
grid-template-columns: var(--table-columns, minmax(180px, 2fr) repeat(5, minmax(110px, 1fr)));
gap: 10px 14px;
align-items: center;
padding: 12px 14px;
margin-bottom: 8px;
border: 0;
border-radius: 10px;
background: transparent;
color: #3f4e5d;
font-size: 0.82rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
}
body.table-view .items-container {
display: block !important;
padding: 0;
overflow-x: visible;
scroll-snap-type: none;
}
body.table-view .item-card {
width: 100%;
max-width: none;
margin: 0 0 10px 0;
padding: 12px 14px;
border: 1px solid #e1e6ed;
border-radius: 10px;
background: #ffffff;
box-shadow: none;
scroll-snap-align: none;
}
body.table-view .item-card .card-content {
display: grid;
grid-template-columns: var(--table-columns, minmax(180px, 2fr) repeat(5, minmax(110px, 1fr)));
gap: 10px 14px;
align-items: center;
}
body.table-view .item-card .card-content h3,
body.table-view .item-card .card-content p {
margin: 0;
max-height: none;
}
body.table-view .item-card .card-content p strong {
display: none;
}
body.table-view .item-card .card-content .item-name-cell,
body.table-view .item-card .card-content .item-col-name {
font-size: 0.98rem;
font-weight: 700;
color: #1f2d3d;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
body.table-view .item-card .card-content .item-ort-cell,
body.table-view .item-card .card-content .item-col-location,
body.table-view .item-card .card-content .item-filter1-cell,
body.table-view .item-card .card-content .item-col-filter1,
body.table-view .item-card .card-content .item-filter2-cell,
body.table-view .item-card .card-content .item-col-filter2,
body.table-view .item-card .card-content .item-filter3-cell,
body.table-view .item-card .card-content .item-col-filter3,
body.table-view .item-card .card-content .item-code-cell,
body.table-view .item-card .card-content .item-col-code,
body.table-view .item-card .card-content .item-col-count {
color: #445465;
font-size: 0.9rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
body.table-view .item-card .image-container,
body.table-view .item-card .borrower-badge,
body.table-view .item-card .appointment-badge,
body.table-view .item-card .damage-badge {
display: none !important;
}
body.table-view .item-card .actions {
margin-top: 10px;
padding-top: 10px;
border-top: 1px dashed #d9e0e8;
}
@media (max-width: 900px) {
.view-switch-group {
position: static;
margin: 8px auto 0;
width: fit-content;
}
body.table-view .table-view-header {
display: none !important;
}
body.table-view .item-card .card-content {
grid-template-columns: 1fr !important;
gap: 6px;
}
body.table-view .item-card .card-content p strong {
display: inline;
}
}
/* Unified action button styling for main and main_admin */
:root {
--action-radius: 10px;
--action-shadow: none;
--action-shadow-hover: none;
}
.actions,
.modal-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.actions form,
.modal-actions form {
margin: 0;
}
.actions .ausleihen,
.actions .schedule-button,
.actions .quick-details-button,
.actions .edit-button,
.actions .duplicate-button,
.actions .delete-button,
.actions .damage-button,
.modal-actions .ausleihen,
.modal-actions .schedule-button,
.modal-actions .quick-details-button,
.modal-actions .edit-button,
.modal-actions .duplicate-button,
.modal-actions .delete-button,
.modal-actions .damage-button {
min-height: 38px !important;
padding: 8px 14px !important;
border-radius: var(--action-radius) !important;
border-width: 1px !important;
border-style: solid !important;
font-size: 0.88rem !important;
font-weight: 700 !important;
letter-spacing: 0.01em;
box-shadow: var(--action-shadow);
transition: transform 0.14s ease, box-shadow 0.14s ease, filter 0.14s ease;
}
.actions .ausleihen,
.modal-actions .ausleihen {
background: #198754 !important;
border-color: #157347 !important;
color: #ffffff !important;
}
.actions .schedule-button,
.modal-actions .schedule-button {
background: #fd7e14 !important;
border-color: #e46f10 !important;
color: #ffffff !important;
}
.actions .quick-details-button,
.modal-actions .quick-details-button {
background: #6c757d !important;
border-color: #616971 !important;
color: #ffffff !important;
}
.actions .edit-button,
.modal-actions .edit-button {
background: #0d6efd !important;
border-color: #0b5ed7 !important;
color: #ffffff !important;
}
.actions .duplicate-button,
.modal-actions .duplicate-button {
background: #6f42c1 !important;
border-color: #6439af !important;
color: #ffffff !important;
}
.actions .delete-button,
.modal-actions .delete-button {
background: #dc3545 !important;
border-color: #bb2d3b !important;
color: #ffffff !important;
}
.actions .damage-button,
.modal-actions .damage-button {
background: #0dcaf0 !important;
border-color: #0aa5c0 !important;
color: #0b1f2a !important;
}
.actions .disabled-button,
.modal-actions .disabled-button,
.actions button:disabled,
.modal-actions button:disabled {
opacity: 0.72;
filter: grayscale(0.28);
box-shadow: none;
cursor: not-allowed;
}
.actions .ausleihen:hover,
.actions .schedule-button:hover,
.actions .quick-details-button:hover,
.actions .edit-button:hover,
.actions .duplicate-button:hover,
.actions .delete-button:hover,
.actions .damage-button:hover,
.modal-actions .ausleihen:hover,
.modal-actions .schedule-button:hover,
.modal-actions .quick-details-button:hover,
.modal-actions .edit-button:hover,
.modal-actions .duplicate-button:hover,
.modal-actions .delete-button:hover,
.modal-actions .damage-button:hover {
transform: translateY(-1px);
box-shadow: var(--action-shadow-hover);
filter: brightness(1.02);
}
.actions button:active,
.modal-actions button:active {
transform: translateY(0);
box-shadow: var(--action-shadow);
}
@media (max-width: 700px) {
.actions,
.modal-actions {
gap: 6px;
}
.actions .ausleihen,
.actions .schedule-button,
.actions .quick-details-button,
.actions .edit-button,
.actions .duplicate-button,
.actions .delete-button,
.actions .damage-button,
.modal-actions .ausleihen,
.modal-actions .schedule-button,
.modal-actions .quick-details-button,
.modal-actions .edit-button,
.modal-actions .duplicate-button,
.modal-actions .delete-button,
.modal-actions .damage-button {
min-height: 36px !important;
padding: 7px 12px !important;
font-size: 0.84rem !important;
}
}
/* Global uniform button system for all pages */
:root {
--ui-btn-radius: 10px;
--ui-btn-min-height: 38px;
--ui-btn-padding-y: 8px;
--ui-btn-padding-x: 14px;
--ui-btn-font-size: 0.9rem;
--ui-btn-font-weight: 700;
--ui-btn-shadow: none;
--ui-btn-shadow-hover: none;
}
.btn,
.action-button,
.button,
.export-button,
.search-button,
.scan-button,
.save-button,
.cancel-button,
.primary-button,
.danger-button,
.submit-button,
.nav-back-button,
.back-button,
.add-new-btn,
.fetch-isbn-button,
.import-book-button,
.return-button,
.item-reset-button,
.admin-only-button,
.reset-cancel-btn,
.reset-confirm-btn,
.btn-accept,
.btn-decline,
.popup-close-button,
.delete-image-button,
.remove-book-cover-button,
.remove-duplicate-image-button {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
gap: 6px;
min-height: var(--ui-btn-min-height) !important;
padding: var(--ui-btn-padding-y) var(--ui-btn-padding-x) !important;
border-radius: var(--ui-btn-radius) !important;
border: 1px solid transparent !important;
font-size: var(--ui-btn-font-size) !important;
font-weight: var(--ui-btn-font-weight) !important;
line-height: 1.2;
letter-spacing: 0.01em;
text-decoration: none !important;
cursor: pointer;
box-shadow: var(--ui-btn-shadow);
transition: transform 0.14s ease, box-shadow 0.14s ease, filter 0.14s ease;
}
.btn:hover,
.action-button:hover,
.button:hover,
.export-button:hover,
.search-button:hover,
.scan-button:hover,
.save-button:hover,
.cancel-button:hover,
.primary-button:hover,
.danger-button:hover,
.submit-button:hover,
.nav-back-button:hover,
.back-button:hover,
.add-new-btn:hover,
.fetch-isbn-button:hover,
.import-book-button:hover,
.return-button:hover,
.item-reset-button:hover,
.admin-only-button:hover,
.reset-cancel-btn:hover,
.reset-confirm-btn:hover,
.btn-accept:hover,
.btn-decline:hover,
.popup-close-button:hover,
.delete-image-button:hover,
.remove-book-cover-button:hover,
.remove-duplicate-image-button:hover {
transform: translateY(-1px);
box-shadow: var(--ui-btn-shadow-hover);
filter: brightness(1.02);
}
.btn:active,
.action-button:active,
.button:active,
.export-button:active,
.search-button:active,
.scan-button:active,
.save-button:active,
.cancel-button:active,
.primary-button:active,
.danger-button:active,
.submit-button:active,
.nav-back-button:active,
.back-button:active,
.add-new-btn:active,
.fetch-isbn-button:active,
.import-book-button:active,
.return-button:active,
.item-reset-button:active,
.admin-only-button:active,
.reset-cancel-btn:active,
.reset-confirm-btn:active,
.btn-accept:active,
.btn-decline:active,
.popup-close-button:active,
.delete-image-button:active,
.remove-book-cover-button:active,
.remove-duplicate-image-button:active {
transform: translateY(0);
box-shadow: var(--ui-btn-shadow);
}
.btn:disabled,
.action-button:disabled,
.button:disabled,
.export-button:disabled,
.search-button:disabled,
.scan-button:disabled,
.save-button:disabled,
.cancel-button:disabled,
.primary-button:disabled,
.danger-button:disabled,
.submit-button:disabled,
.add-new-btn:disabled,
.fetch-isbn-button:disabled,
.import-book-button:disabled,
.return-button:disabled,
.item-reset-button:disabled,
.admin-only-button:disabled,
.reset-cancel-btn:disabled,
.reset-confirm-btn:disabled,
.btn-accept:disabled,
.btn-decline:disabled,
.popup-close-button:disabled,
.delete-image-button:disabled,
.remove-book-cover-button:disabled,
.remove-duplicate-image-button:disabled {
opacity: 0.72;
box-shadow: none;
cursor: not-allowed;
filter: grayscale(0.25);
}
/* Preserve consistent role colors */
.btn-primary,
.save-button,
.submit-button,
.primary-button,
.register-button {
background: #0d6efd !important;
border-color: #0b5ed7 !important;
color: #fff !important;
}
.btn-secondary,
.cancel-button,
.button,
.nav-back-button,
.back-button,
.reset-cancel-btn {
background: #6c757d !important;
border-color: #616971 !important;
color: #fff !important;
}
.btn-success,
.btn-accept,
.return-button,
.item-reset-button,
.import-book-button,
.fetch-isbn-button,
.scan-button {
background: #198754 !important;
border-color: #157347 !important;
color: #fff !important;
}
.btn-warning,
.export-button,
.search-button,
.add-new-btn {
background: #fd7e14 !important;
border-color: #e46f10 !important;
color: #fff !important;
}
.btn-danger,
.danger-button,
.btn-decline,
.reset-confirm-btn,
.delete-image-button,
.remove-book-cover-button,
.remove-duplicate-image-button {
background: #dc3545 !important;
border-color: #bb2d3b !important;
color: #fff !important;
}
.btn-sm {
min-height: 32px !important;
padding: 6px 10px !important;
font-size: 0.82rem !important;
}
.w-100.btn,
.w-100.action-button,
.w-100.button,
.w-100.export-button,
.w-100.save-button,
.w-100.submit-button,
.w-100.primary-button,
.w-100.danger-button,
.w-100.cancel-button {
width: 100% !important;
}
@media (max-width: 700px) {
.btn,
.action-button,
.button,
.export-button,
.search-button,
.scan-button,
.save-button,
.cancel-button,
.primary-button,
.danger-button,
.submit-button,
.nav-back-button,
.back-button,
.add-new-btn,
.fetch-isbn-button,
.import-book-button,
.return-button,
.item-reset-button,
.admin-only-button,
.reset-cancel-btn,
.reset-confirm-btn,
.btn-accept,
.btn-decline,
.popup-close-button,
.delete-image-button,
.remove-book-cover-button,
.remove-duplicate-image-button {
min-height: 36px !important;
padding: 7px 12px !important;
font-size: 0.84rem !important;
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200"><rect width="200" height="200" fill="#f0f0f0"/><text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="Arial" font-size="20" fill="#666">No Image</text></svg>

After

Width:  |  Height:  |  Size: 273 B

+400
View File
@@ -0,0 +1,400 @@
/**
* Copyright 2025-2026 AIIrondev
*
* Licensed under the Inventarsystem EULA (Endbenutzer-Lizenzvertrag).
* See Legal/LICENSE for the full license text.
* Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
* For commercial licensing inquiries: https://github.com/AIIrondev
*/
// Initialize when document is loaded
document.addEventListener('DOMContentLoaded', function() {
if (isIOSDevice()) {
console.log('iOS device detected, applying iOS-specific enhancements');
applyIOSFixes();
}
});
// Apply all iOS-specific fixes
function applyIOSFixes() {
// Fix file input issues
fixIOSFileInputs();
// Fix form submission
fixIOSFormSubmission();
// Fix image handling
fixIOSImageHandling();
// Fix duplication
fixIOSDuplication();
// Apply CSS fixes
applyIOSCSSFixes();
}
// Fix iOS file input issues
function fixIOSFileInputs() {
// Find all file inputs
const fileInputs = document.querySelectorAll('input[type="file"]');
fileInputs.forEach(input => {
// Create a touch-friendly wrapper
const wrapper = document.createElement('div');
wrapper.className = 'ios-file-input-wrapper';
wrapper.style.position = 'relative';
// Style the original input to be more touch-friendly
input.style.padding = '20px 0';
// Wrap the input
if (input.parentNode) {
input.parentNode.insertBefore(wrapper, input);
wrapper.appendChild(input);
// Add a visible button for better touch target
const fakeButton = document.createElement('div');
fakeButton.className = 'ios-file-button';
fakeButton.textContent = 'Wählen Sie Bilder/Videos aus';
fakeButton.style.display = 'inline-block';
fakeButton.style.padding = '10px 15px';
fakeButton.style.backgroundColor = '#4CAF50';
fakeButton.style.color = 'white';
fakeButton.style.borderRadius = '4px';
fakeButton.style.textAlign = 'center';
fakeButton.style.margin = '10px 0';
fakeButton.style.fontWeight = 'bold';
wrapper.appendChild(fakeButton);
// Make sure the real input covers the fake button
input.style.position = 'absolute';
input.style.top = '0';
input.style.left = '0';
input.style.width = '100%';
input.style.height = '100%';
input.style.opacity = '0';
input.style.zIndex = '10';
}
// Add custom event handling
input.addEventListener('change', function() {
// Update visual feedback
const fileCount = this.files ? this.files.length : 0;
let fileText = fakeButton.textContent;
if (fileCount > 0) {
fileText = `${fileCount} Datei${fileCount !== 1 ? 'en' : ''} ausgewählt`;
fakeButton.style.backgroundColor = '#2E7D32';
} else {
fileText = 'Wählen Sie Bilder/Videos aus';
fakeButton.style.backgroundColor = '#4CAF50';
}
fakeButton.textContent = fileText;
});
});
}
// Fix iOS form submission issues
function fixIOSFormSubmission() {
// Find upload forms
const uploadForms = document.querySelectorAll('form[action*="upload_item"]');
uploadForms.forEach(form => {
// Add an iOS submission handler
form.addEventListener('submit', function(e) {
// Only intercept on iOS
if (!isIOSDevice()) return;
e.preventDefault();
// Show that we're processing
const loadingMessage = document.createElement('div');
loadingMessage.className = 'ios-loading-message';
loadingMessage.textContent = 'Wird verarbeitet...';
loadingMessage.style.padding = '10px';
loadingMessage.style.backgroundColor = '#f0f0f0';
loadingMessage.style.borderRadius = '5px';
loadingMessage.style.margin = '10px 0';
loadingMessage.style.textAlign = 'center';
form.appendChild(loadingMessage);
// Disable the submit button
const submitButton = form.querySelector('button[type="submit"]');
if (submitButton) {
submitButton.disabled = true;
}
// Use timeout to allow UI to update before heavy processing
setTimeout(() => {
// Gather form data with special handling for iOS
const formData = new FormData(form);
// Add iOS flag
formData.append('is_ios', 'true');
// Submit with fetch API which works better on iOS
fetch(form.action, {
method: 'POST',
body: formData,
credentials: 'same-origin'
})
.then(response => response.json())
.then(data => {
// Remove loading message
if (loadingMessage.parentNode) {
loadingMessage.parentNode.removeChild(loadingMessage);
}
// Re-enable submit button
if (submitButton) {
submitButton.disabled = false;
}
if (data.success) {
// Show success and redirect
alert(data.message || 'Element erfolgreich hinzugefügt');
// Redirect to the appropriate page
if (data.itemId) {
window.location.href = `/home_admin?highlight_item=${data.itemId}`;
} else {
window.location.href = '/home_admin';
}
} else {
// Show error
alert(data.message || 'Ein Fehler ist aufgetreten');
}
})
.catch(error => {
console.error('iOS form submission error:', error);
// Remove loading message
if (loadingMessage.parentNode) {
loadingMessage.parentNode.removeChild(loadingMessage);
}
// Re-enable submit button
if (submitButton) {
submitButton.disabled = false;
}
// Show error
alert('Ein Netzwerkfehler ist aufgetreten. Bitte versuchen Sie es erneut.');
});
}, 100);
});
});
}
// Fix iOS image handling issues
function fixIOSImageHandling() {
// Reduce image preview quality on iOS
if (typeof ImagePreviewGenerator !== 'undefined') {
// Override the compression quality for iOS
const originalCompressImage = ImagePreviewGenerator.prototype._compressImage;
if (originalCompressImage) {
ImagePreviewGenerator.prototype._compressImage = function(dataUrl) {
// Lower quality for iOS
this.quality = 0.5;
this.maxWidth = 600;
this.maxHeight = 600;
return originalCompressImage.call(this, dataUrl);
};
}
}
// Fix image preview display
document.querySelectorAll('.image-preview-container').forEach(container => {
// Add iOS-specific styling
container.style.webkitOverflowScrolling = 'touch';
container.style.maxHeight = '150px';
container.style.overflow = 'auto';
});
}
// Fix iOS duplication issues
function fixIOSDuplication() {
// Override duplicateItem function
if (typeof duplicateItem !== 'undefined') {
const originalDuplicateItem = duplicateItem;
duplicateItem = function(itemId) {
// Use localStorage on iOS instead of sessionStorage
const useFunction = originalDuplicateItem;
useFunction(itemId);
// Additional iOS-specific handling
const checkForSessionData = setInterval(() => {
const sessionData = sessionStorage.getItem('duplicateItemData');
if (sessionData) {
// Copy from sessionStorage to localStorage
try {
localStorage.setItem('duplicateItemData', sessionData);
localStorage.setItem('duplicateItemTimestamp', Date.now().toString());
console.log('Copied duplicate data to localStorage for iOS reliability');
} catch (e) {
console.warn('Failed to copy to localStorage:', e);
}
clearInterval(checkForSessionData);
}
}, 100);
// Clear interval after 3 seconds regardless
setTimeout(() => clearInterval(checkForSessionData), 3000);
};
}
// Fix prefill function
if (typeof prefillFormWithDuplicateData !== 'undefined') {
const originalPrefill = prefillFormWithDuplicateData;
prefillFormWithDuplicateData = function() {
// Try to get data from localStorage first
let data = null;
try {
const localData = localStorage.getItem('duplicateItemData');
if (localData) {
data = JSON.parse(localData);
localStorage.removeItem('duplicateItemData');
localStorage.removeItem('duplicateItemTimestamp');
console.log('Using duplicate data from localStorage');
}
} catch (e) {
console.warn('Error accessing localStorage:', e);
}
// If we got data from localStorage, manually populate the form
if (data) {
console.log('Manually populating form with iOS optimization');
// Basic fields
document.getElementById('name').value = data.name || '';
document.getElementById('beschreibung').value = data.description || '';
// Handle filters with a delay
setTimeout(() => {
// Filter 1
if (data.filter1 && Array.isArray(data.filter1)) {
data.filter1.forEach((val, idx) => {
const field = document.getElementById(`filter1-${idx+1}`);
if (field && val) field.value = val;
});
}
// Filter 2
if (data.filter2 && Array.isArray(data.filter2)) {
data.filter2.forEach((val, idx) => {
const field = document.getElementById(`filter2-${idx+1}`);
if (field && val) field.value = val;
});
}
// Filter 3
if (data.filter3 && Array.isArray(data.filter3)) {
data.filter3.forEach((val, idx) => {
const field = document.getElementById(`filter3-${idx+1}`);
if (field && val) field.value = val;
});
}
}, 300);
// Location
setTimeout(() => {
const location = document.getElementById('ort');
if (location && data.location) location.value = data.location;
}, 400);
// Year and cost
const year = document.getElementById('anschaffungsjahr');
if (year && data.year) year.value = data.year;
const cost = document.getElementById('anschaffungskosten');
if (cost && data.cost) cost.value = data.cost;
// Handle images - limited for iOS
const form = document.querySelector('form');
if (form && data.images && Array.isArray(data.images)) {
// Add hidden field to indicate duplication
const isDuplicatingField = document.createElement('input');
isDuplicatingField.type = 'hidden';
isDuplicatingField.name = 'is_duplicating';
isDuplicatingField.value = 'true';
form.appendChild(isDuplicatingField);
// Only use first 3 images on iOS for performance
const limitedImages = data.images.slice(0, 3);
limitedImages.forEach(imageName => {
const imageField = document.createElement('input');
imageField.type = 'hidden';
imageField.name = 'duplicate_images';
imageField.value = imageName;
form.appendChild(imageField);
});
// Add a message about limited images
if (data.images.length > 3) {
const messageDiv = document.createElement('div');
messageDiv.style.margin = '10px 0';
messageDiv.style.padding = '10px';
messageDiv.style.backgroundColor = '#fff3cd';
messageDiv.style.borderRadius = '5px';
messageDiv.style.color = '#856404';
messageDiv.textContent = `Aus Leistungsgründen werden nur ${limitedImages.length} von ${data.images.length} Bildern verwendet`;
const fileInput = document.getElementById('images');
if (fileInput && fileInput.parentNode) {
fileInput.parentNode.appendChild(messageDiv);
} else {
form.appendChild(messageDiv);
}
}
}
} else {
// Use original function as fallback
originalPrefill();
}
};
}
}
// Apply iOS-specific CSS fixes
function applyIOSCSSFixes() {
// Create a style element
const style = document.createElement('style');
style.textContent = `
/* Improve touch targets for iOS */
button, select, input[type="submit"], .ausleihen, .edit-button,
.delete-button, .duplicate-button, .details-button {
min-height: 44px !important;
padding: 10px 15px !important;
}
/* Improve scrolling on iOS */
.items-container, .filter-options, .modal-content {
-webkit-overflow-scrolling: touch !important;
}
/* Prevent zoom on inputs */
input, select, textarea {
font-size: 16px !important;
}
/* Fix iOS image display */
.item-image {
-webkit-transform: translateZ(0);
}
/* Improve form fields on iOS */
.form-group {
margin-bottom: 15px !important;
}
`;
document.head.appendChild(style);
}
+285
View File
@@ -0,0 +1,285 @@
/**
* Copyright 2025-2026 AIIrondev
*
* Licensed under the Inventarsystem EULA (Endbenutzer-Lizenzvertrag).
* See Legal/LICENSE for the full license text.
* Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
* For commercial licensing inquiries: https://github.com/AIIrondev
*/
// Detect mobile devices including tablets
function isMobileDevice() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
// Specifically detect iOS devices
function isIOSDevice() {
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
}
// Mobile-compatible file tracker to replace DataTransfer API
class MobileCompatFileTracker {
constructor() {
this.files = [];
this.removedIndices = new Set();
}
// Add files from a FileList
addFiles(fileList) {
// Convert FileList to Array for easier handling
Array.from(fileList).forEach(file => {
this.files.push(file);
});
}
// Remove a file by index
removeFile(index) {
if (index >= 0 && index < this.files.length) {
this.removedIndices.add(index);
}
}
// Get all non-removed files
getFiles() {
return this.files.filter((_, index) => !this.removedIndices.has(index));
}
// Convert to FormData for upload
appendToFormData(formData, fieldName) {
this.getFiles().forEach(file => {
formData.append(fieldName, file);
});
}
// Clear all files
clear() {
this.files = [];
this.removedIndices.clear();
}
}
// Mobile optimized image preview generator
class ImagePreviewGenerator {
constructor(options = {}) {
this.maxWidth = options.maxWidth || 800;
this.maxHeight = options.maxHeight || 600;
this.quality = options.quality || 0.8;
this.maxPreviewsOnMobile = options.maxPreviewsOnMobile || 3;
}
// Create optimized image preview element
createPreview(file, index) {
return new Promise((resolve, reject) => {
if (!file) {
reject(new Error('No file provided'));
return;
}
const isVideo = file.type.startsWith('video/');
if (isVideo) {
this._createVideoPreview(file, index).then(resolve).catch(reject);
} else if (file.type.startsWith('image/')) {
this._createImagePreview(file, index).then(resolve).catch(reject);
} else {
reject(new Error('Unsupported file type'));
}
});
}
// Create image preview with optimization for mobile
_createImagePreview(file, index) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
// For mobile, we'll compress the image before creating the preview
if (isMobileDevice()) {
this._compressImage(e.target.result)
.then(compressedDataUrl => {
const previewElement = this._createPreviewElement(compressedDataUrl, file.name, index, false);
resolve(previewElement);
})
.catch(err => {
// Fallback to original if compression fails
console.warn('Image compression failed, using original:', err);
const previewElement = this._createPreviewElement(e.target.result, file.name, index, false);
resolve(previewElement);
});
} else {
// For desktop, use the original image
const previewElement = this._createPreviewElement(e.target.result, file.name, index, false);
resolve(previewElement);
}
};
reader.onerror = () => reject(new Error('Failed to read file'));
reader.readAsDataURL(file);
});
}
// Create video preview with thumbnail generation
_createVideoPreview(file, index) {
return new Promise((resolve, reject) => {
const videoUrl = URL.createObjectURL(file);
const video = document.createElement('video');
video.src = videoUrl;
video.onloadedmetadata = () => {
// Create preview element
const previewElement = this._createPreviewElement(videoUrl, file.name, index, true);
resolve(previewElement);
// Revoke object URL after a delay to ensure it's loaded
setTimeout(() => {
URL.revokeObjectURL(videoUrl);
}, 3000);
};
video.onerror = () => {
URL.revokeObjectURL(videoUrl);
reject(new Error('Failed to load video'));
};
// Set a timeout in case the video never loads
setTimeout(() => {
if (!video.videoWidth) {
URL.revokeObjectURL(videoUrl);
reject(new Error('Video load timeout'));
}
}, 5000);
});
}
// Create HTML element for preview
_createPreviewElement(src, fileName, index, isVideo) {
const previewDiv = document.createElement('div');
previewDiv.className = 'image-preview-item';
if (isVideo) {
const video = document.createElement('video');
video.src = src;
video.controls = true;
video.preload = 'metadata';
video.style.maxWidth = '150px';
video.style.maxHeight = '150px';
video.style.objectFit = 'cover';
previewDiv.appendChild(video);
} else {
const img = document.createElement('img');
img.src = src;
img.alt = fileName;
img.style.maxWidth = '150px';
img.style.maxHeight = '150px';
img.style.objectFit = 'cover';
previewDiv.appendChild(img);
}
const controlsDiv = document.createElement('div');
controlsDiv.className = 'image-controls';
const removeButton = document.createElement('button');
removeButton.type = 'button';
removeButton.textContent = 'Entfernen';
removeButton.style.background = '#dc3545';
removeButton.style.color = 'white';
removeButton.style.border = 'none';
removeButton.style.padding = '5px 10px';
removeButton.style.borderRadius = '3px';
removeButton.style.cursor = 'pointer';
removeButton.style.marginLeft = '10px';
removeButton.dataset.index = index;
removeButton.addEventListener('click', function() {
const indexToRemove = parseInt(this.dataset.index, 10);
// We'll define removeImagePreview elsewhere
if (typeof window.removeImagePreview === 'function') {
window.removeImagePreview(indexToRemove);
}
});
controlsDiv.appendChild(removeButton);
previewDiv.appendChild(controlsDiv);
return previewDiv;
}
// Compress image for mobile devices
_compressImage(dataUrl) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
let width = img.width;
let height = img.height;
// Scale down if needed
if (width > this.maxWidth || height > this.maxHeight) {
const ratio = Math.min(this.maxWidth / width, this.maxHeight / height);
width *= ratio;
height *= ratio;
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
// Get compressed data URL
const compressedDataUrl = canvas.toDataURL('image/jpeg', this.quality);
// Clean up
canvas.width = 0;
canvas.height = 0;
resolve(compressedDataUrl);
};
img.onerror = () => reject(new Error('Failed to load image for compression'));
img.src = dataUrl;
});
}
}
// Mobile-optimized debounce function for event handlers
function debounce(func, wait = 300) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
// Mobile-safe promise-based setTimeout
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Log mobile-specific issues to console or server
function logMobileIssue(action, error) {
if (isMobileDevice()) {
const diagnosticInfo = {
action: action,
error: error.message,
browser: navigator.userAgent,
timestamp: new Date().toISOString(),
memoryInfo: performance?.memory ? {
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
totalJSHeapSize: performance.memory.totalJSHeapSize,
usedJSHeapSize: performance.memory.usedJSHeapSize
} : 'Not available'
};
console.error('Mobile Issue:', diagnosticInfo);
// Optionally send to server
fetch('/log_mobile_issue', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(diagnosticInfo)
}).catch(() => {
// Silently fail if logging fails
});
}
}
+24
View File
@@ -0,0 +1,24 @@
/**
* Copyright 2025-2026 AIIrondev
*
* Licensed under the Inventarsystem EULA (Endbenutzer-Lizenzvertrag).
* See Legal/LICENSE for the full license text.
* Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
* For commercial licensing inquiries: https://github.com/AIIrondev
*/
chatBox = document.getElementById('chatBox');
function fetchMessages() {
fetch('/messages')
.then(response => response.json())
.then(data => {
chatBox.innerHTML = '';
data.messages.forEach(msg => {
const messageElement = document.createElement('div');
messageElement.textContent = msg;
chatBox.appendChild(messageElement);
});
});
}
setInterval(fetchMessages, 1000);