added new tablet mode for creating shipping notes

This commit is contained in:
2025-08-05 13:32:50 +02:00
parent 1c4c7368a2
commit dff3a4f5b8
11 changed files with 260 additions and 21 deletions

View File

@@ -0,0 +1,123 @@
<!DOCTYPE html>
<html lang="de" class="h-full">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Lager-System Login</title>
<meta name="theme-color" content="#0055FF"/>
<link rel="manifest" href="/assets/pwa/shipping-note-tablet-manifest.json">
<script src="https://cdn.jsdelivr.net/npm/vue@3.4.27/dist/vue.global.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<script>
window.WAREHOUSE_CONFIG = { userOptions: <?= json_encode($userOptions) ?> };
tailwind.config = {
theme: { extend: { colors: { 'brand-blue': '#0055FF' } } }
};
</script>
<style>
body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
.user-list-container::-webkit-scrollbar { width: 5px; }
.user-list-container::-webkit-scrollbar-track { background: #f1f5f9; }
.user-list-container::-webkit-scrollbar-thumb { background: #a8b2c1; border-radius: 10px; }
.list-move, .list-enter-active, .list-leave-active { transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1); }
.list-enter-from, .list-leave-to { opacity: 0; transform: scale(0.95); }
.list-leave-active { position: absolute !important; }
.container-enter-active { transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1); }
.container-enter-from { opacity: 0; transform: translateY(20px); }
</style>
</head>
<body class="bg-slate-100 flex items-center justify-center min-h-full font-sans p-4">
<div id="app" class="w-full max-w-lg mx-auto"></div>
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => navigator.serviceWorker.register('/assets/pwa/shipping-note-tablet-sw.js'));
}
const {createApp, ref, computed, onMounted} = Vue;
createApp({
setup() {
const excludedNames = ['cli', 'spitzer', 'test'];
const initialUsers = window.WAREHOUSE_CONFIG.userOptions.filter(user => {
const nameLower = user.text.toLowerCase();
return !excludedNames.some(ex => nameLower.includes(ex));
});
const selectedLetter = ref('All');
const selectedUserId = ref(null);
const isMounted = ref(false);
onMounted(() => isMounted.value = true);
const availableLetters = computed(() => {
const letters = new Set(initialUsers.map(u => u.text[0].toUpperCase()));
return ['All', ...Array.from(letters).sort()];
});
const filteredUsers = computed(() => {
if (selectedLetter.value === 'All') return initialUsers;
return initialUsers.filter(u => u.text.startsWith(selectedLetter.value));
});
function selectUser(userId) {
selectedUserId.value = userId;
}
return { isMounted, availableLetters, selectedLetter, filteredUsers, selectedUserId, selectUser };
},
template: `
<Transition name="container" appear>
<div v-if="isMounted" class="bg-white p-6 sm:p-8 rounded-2xl shadow-2xl shadow-slate-300/60 w-full text-center">
<header class="mb-6">
<img src="/assets/images/xinon-full.png" alt="Logo" class="h-12 w-auto mx-auto mb-4">
<h1 class="text-3xl sm:text-4xl font-bold text-slate-800 tracking-tight">Lager-System</h1>
<p class="text-slate-500 mt-2">Bitte wählen Sie Ihren Namen aus.</p>
</header>
<main>
<form action="/WarehouseShippingNote" method="POST">
<input type="hidden" name="wantedUserId" :value="selectedUserId"/>
<div class="mb-5 p-1.5 bg-slate-100 rounded-xl flex flex-wrap justify-center gap-1.5">
<button v-for="letter in availableLetters" :key="letter" type="button"
@click="selectedLetter = letter; selectedUserId = null;"
class="w-12 h-12 rounded-lg text-base font-bold transition-all duration-300"
:class="selectedLetter === letter ? 'bg-brand-blue text-white shadow' : 'bg-transparent text-slate-500 hover:bg-slate-200'">
{{ letter === 'All' ? 'Alle' : letter }}
</button>
</div>
<div class="user-list-container max-h-[40vh] min-h-[150px] overflow-y-auto relative p-1.5">
<TransitionGroup name="list" tag="div" class="relative overflow-x-hidden">
<button v-for="(user, index) in filteredUsers" :key="user.value" type="button"
@click="selectUser(user.value)"
class="w-full text-left p-4 rounded-xl transition-all duration-200 ease-in-out mb-2 last:mb-0"
:style="{ 'transition-delay': (index * 15) + 'ms' }"
:class="selectedUserId === user.value ? 'bg-brand-blue text-white shadow-lg' : 'bg-slate-50 hover:bg-slate-200 text-slate-700'">
<span class="text-lg font-medium">{{ user.text }}</span>
</button>
</TransitionGroup>
<div v-if="!filteredUsers.length" class="text-center text-slate-400 pt-10">
<p>Keine Mitarbeiter für diese Auswahl.</p>
</div>
</div>
<div class="mt-6">
<button type="submit" :disabled="!selectedUserId"
class="w-full text-white font-bold py-3.5 px-4 rounded-xl text-xl transition-all duration-300 bg-brand-blue hover:bg-blue-600 focus:outline-none focus:ring-4 focus:ring-blue-500/50 disabled:bg-slate-300 disabled:text-slate-500 disabled:cursor-not-allowed">
Anmelden
</button>
</div>
</form>
</main>
</div>
</Transition>
`
}).mount('#app');
</script>
</body>
</html>

View File

@@ -15,7 +15,7 @@
</footer>
<!-- end Footer -->
<?php if(defined("ENABLE_VODIA_IDENTITY_SWITCHER") && ENABLE_VODIA_IDENTITY_SWITCHER && $me->is("employee")): ?>
<?php if(defined("ENABLE_VODIA_IDENTITY_SWITCHER") && ENABLE_VODIA_IDENTITY_SWITCHER && $me->is("employee") && !isset($_SESSION[MFAPPNAME . '_warehouse_login_override'])): ?>
<script type="text/javascript" src="<?=self::getResourcePath()?>assets/js/xinon-vodia-identity.js?<?=$git_merge_ts?>" defer></script>
<?php endif; ?>

View File

@@ -4,13 +4,36 @@ class AddressController extends mfBaseController {
private $filter;
protected function init() {
$this->needlogin = true;
$me = new User();
$me->loadMe();
$this->me = $me;
$this->layout()->set("me", $me);
$user = null;
$overrideKey = MFAPPNAME . '_warehouse_login_override';
$timestampKey = MFAPPNAME . '_warehouse_login_override_timestamp';
if (!$me->is(["Admin", "salespartner"])) {
$overrideId = $_SESSION[$overrideKey] ?? null;
$timestamp = $_SESSION[$timestampKey] ?? null;
if (is_numeric($overrideId) && $timestamp && (time() - $timestamp) < 600) {
$potentialUser = new User($overrideId);
if ($potentialUser->id && $potentialUser->address_id == 1) {
$user = $potentialUser;
} else {
unset($_SESSION[$overrideKey], $_SESSION[$timestampKey]);
$this->redirect('WarehouseShippingNote');
return;
}
} elseif ($overrideId) {
unset($_SESSION[$overrideKey], $_SESSION[$timestampKey]);
}
if (!$user) {
$this->needlogin = true;
$user = new User();
$user->loadMe();
}
$this->me = $user;
$this->layout()->set("me", $this->me);
if (!$this->me->is(["Admin", "salespartner"])) {
$this->redirect("Dashboard");
}
}

View File

@@ -125,6 +125,8 @@ class UserModel
$where .= " AND WorkerPermission.employee = 'true'";
}
}
if (isset($filter['active']))
$where .= " AND Worker.active = " . (int)$filter['active'];
//var_dump($filter, $where);exit;
return $where;

View File

@@ -28,7 +28,7 @@ class WarehouseShippingNoteController extends TTCrud {
['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']],];
protected array $defaultOrder = ['key' => 'create', 'order' => 'DESC'];
protected array $additionalJSVariables = ['WAREHOUSE_ADMIN' => true];
protected array $additionalJSVariables = ['WAREHOUSE_ADMIN' => true, 'HIDE_MENU' => false];
protected array $infoMessages = ['create' => 'Lieferschein wurde erstellt.',
'update' => 'Lieferschein wurde aktualisiert',
'delete' => 'Lieferschein wurde gelöscht',
@@ -37,6 +37,7 @@ class WarehouseShippingNoteController extends TTCrud {
protected function prepareCrudConfig() {
if (!$this->user->can('WarehouseAdmin')) $this->additionalJSVariables['WAREHOUSE_ADMIN'] = false;
if (isset($_SESSION[MFAPPNAME . '_warehouse_login_override']) && is_numeric($_SESSION[MFAPPNAME . '_warehouse_login_override'])) $this->additionalJSVariables['HIDE_MENU'] = true;
}
protected function beforeCreate($postData): bool {

View File

@@ -27,21 +27,26 @@ class TTCrud extends mfBaseController {
}
protected function init() {
$this->needlogin = true;
$me = new User();
$me->loadMe();
$this->user = $me;
$this->layout()->set('me', $me);
$className = get_class($this);
if (isset($this->permissionCheck) && !$me->can($this->permissionCheck)) {
$this->redirect("Dashboard");
} else if (!$me->can($this->permissionCheck) && !$me->is(["Admin"])) {
$this->redirect("Dashboard");
if (defined('TT_WAREHOUSE_LOGIN_OVERRIDE') && is_array(TT_WAREHOUSE_LOGIN_OVERRIDE)
&& in_array($className, TT_WAREHOUSE_LOGIN_OVERRIDE) && !mfLoginController::isLoggedIn())
$this->user = $this->loginOverride();
else {
$this->needlogin = true;
$this->user = new User();
$this->user->loadMe();
}
$this->layout()->set('me', $this->user);
$c = get_class($this);
foreach ([str_replace('Controller', 'Model', $c), str_replace('Controller', '', $c)] as $m)
if (class_exists($m)) {
if (method_exists($this, 'permissionsCheckOverride'))
$this->permissionsCheckOverride();
else if (!$this->user->is(["Admin"]))
$this->redirect("Dashboard");
foreach ([str_replace('Controller', 'Model', $className), str_replace('Controller', '', $className)] as $m) {
if (class_exists($m))
$this->model = new $m();
break;
}
@@ -52,6 +57,37 @@ class TTCrud extends mfBaseController {
if (method_exists($this, 'afterInit')) $this->afterInit();
}
protected function loginOverride() {
$allowedIPs = ['193.105.204.200', '91.227.230.253', '193.105.204.195', '172.18.0.1'];
if (!in_array($_SERVER['REMOTE_ADDR'], $allowedIPs)) $this->redirect('Dashboard');
if (isset($_POST['wantedUserId']) && is_numeric($_POST['wantedUserId'])) {
$user = new User($_POST['wantedUserId']);
if ($user->id && $user->address_id == 1) {
$_SESSION[MFAPPNAME . '_warehouse_login_override'] = $user->id;
$_SESSION[MFAPPNAME . '_warehouse_login_override_timestamp'] = time();
$this->redirect('WarehouseShippingNote');
}
$this->redirect('Dashboard');
}
$sessionUserId = $_SESSION[MFAPPNAME . '_warehouse_login_override'] ?? null;
$sessionTimestamp = $_SESSION[MFAPPNAME . '_warehouse_login_override_timestamp'] ?? 0;
if (is_numeric($sessionUserId) && (time() - $sessionTimestamp <= 300)) {
$user = new User($sessionUserId);
if ($user->id && $user->address_id == 1) return $user;
$this->redirect('WarehouseShippingNote');
}
$users = UserModel::search(['employee' => true, 'active' => true]);
$userOptions = array_map(fn($user) => ['value' => (int)$user->id, 'text' => $user->name], $users);
$this->layout()->set('userOptions', $userOptions);
$this->layout()->setTemplate("VueViews/WarehouseLoginOverride");
echo $this->layout()->render();
exit;
}
/**
* Returns the checkArray for the CRUD component.
* @return array

View File

@@ -128,7 +128,7 @@ class mfLoginController extends mfBaseController
UserToken::checkToken();
if ($_SESSION[MFAPPNAME . '_username'] && $_SESSION[MFAPPNAME . '_ip']) {
if (isset($_SESSION[MFAPPNAME . '_username']) && $_SESSION[MFAPPNAME . '_username'] && $_SESSION[MFAPPNAME . '_ip']) {
$username = $_SESSION[MFAPPNAME . '_username'];
$ip = $_SERVER['REMOTE_ADDR'];
$sid = session_id();

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

View File

@@ -0,0 +1,22 @@
{
"name": "Lieferscheine",
"short_name": "Lieferscheine",
"description": "XINON Lieferscheine App",
"start_url": ".",
"display": "standalone",
"background_color": "#f1f5f9",
"theme_color": "#0055FF",
"orientation": "portrait-primary",
"icons": [
{
"src": "/assets/images/xinon-sm-192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "/assets/images/xinon-sm-512.png",
"type": "image/png",
"sizes": "512x512"
}
]
}

View File

@@ -0,0 +1,32 @@
const CACHE_NAME = 'lager-system-cache-v1';
const urlsToCache = [
'.',
'https://cdn.jsdelivr.net/npm/vue@3.4.27/dist/vue.global.min.js',
'https://cdn.tailwindcss.com',
'/assets/images/xinon-full.png',
'/assets/icons/icon-192x192.png',
'/assets/icons/icon-512x512.png'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(urlsToCache))
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => response || fetch(event.request))
);
});
self.addEventListener('activate', event => {
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys().then(cacheNames => Promise.all(
cacheNames
.filter(cacheName => !cacheWhitelist.includes(cacheName))
.map(cacheName => caches.delete(cacheName))
))
);
});