Merge pull request #36 from metacube2/claude/fix-layout-centering-cdX7d
Claude/fix layout centering cd x7d
This commit is contained in:
@@ -14,6 +14,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
func application(_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
// Configure for macOS
|
||||
configureMacOS()
|
||||
#endif
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -33,10 +37,60 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Resume game if needed
|
||||
}
|
||||
|
||||
#if targetEnvironment(macCatalyst)
|
||||
// MARK: - macOS Configuration
|
||||
private func configureMacOS() {
|
||||
// Set minimum window size for macOS
|
||||
UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.forEach { windowScene in
|
||||
windowScene.sizeRestrictions?.minimumSize = CGSize(width: 400, height: 600)
|
||||
windowScene.sizeRestrictions?.maximumSize = CGSize(width: 600, height: 900)
|
||||
}
|
||||
}
|
||||
|
||||
override func buildMenu(with builder: UIMenuBuilder) {
|
||||
super.buildMenu(with: builder)
|
||||
|
||||
// Remove unnecessary menus for a game
|
||||
builder.remove(menu: .format)
|
||||
builder.remove(menu: .edit)
|
||||
|
||||
// Add Game menu
|
||||
let pauseCommand = UIKeyCommand(
|
||||
title: "Pause",
|
||||
action: #selector(handlePauseCommand),
|
||||
input: "p",
|
||||
modifierFlags: .command
|
||||
)
|
||||
|
||||
let restartCommand = UIKeyCommand(
|
||||
title: "Neustart",
|
||||
action: #selector(handleRestartCommand),
|
||||
input: "r",
|
||||
modifierFlags: .command
|
||||
)
|
||||
|
||||
let gameMenu = UIMenu(
|
||||
title: "Spiel",
|
||||
children: [pauseCommand, restartCommand]
|
||||
)
|
||||
|
||||
builder.insertSibling(gameMenu, afterMenu: .file)
|
||||
}
|
||||
|
||||
@objc private func handlePauseCommand() {
|
||||
NotificationCenter.default.post(name: .pauseGame, object: nil)
|
||||
}
|
||||
|
||||
@objc private func handleRestartCommand() {
|
||||
NotificationCenter.default.post(name: .restartGame, object: nil)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - Notification Names
|
||||
extension Notification.Name {
|
||||
static let pauseGame = Notification.Name("pauseGame")
|
||||
static let resumeGame = Notification.Name("resumeGame")
|
||||
static let restartGame = Notification.Name("restartGame")
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@ class GameViewController: UIViewController {
|
||||
|
||||
// Setup notification observers
|
||||
setupNotificationObservers()
|
||||
|
||||
#if targetEnvironment(macCatalyst)
|
||||
setupMacCatalyst()
|
||||
#endif
|
||||
}
|
||||
|
||||
private func setupNotificationObservers() {
|
||||
@@ -45,6 +49,13 @@ class GameViewController: UIViewController {
|
||||
name: .pauseGame,
|
||||
object: nil
|
||||
)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleRestartNotification),
|
||||
name: .restartGame,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
@objc private func handlePauseNotification() {
|
||||
@@ -57,12 +68,46 @@ class GameViewController: UIViewController {
|
||||
// This is just a notification that the app is going to background
|
||||
}
|
||||
|
||||
@objc private func handleRestartNotification() {
|
||||
guard let skView = self.view as? SKView else { return }
|
||||
|
||||
let menuScene = MenuScene(size: skView.bounds.size)
|
||||
menuScene.scaleMode = .aspectFill
|
||||
|
||||
let transition = SKTransition.fade(withDuration: 0.5)
|
||||
skView.presentScene(menuScene, transition: transition)
|
||||
}
|
||||
|
||||
#if targetEnvironment(macCatalyst)
|
||||
private func setupMacCatalyst() {
|
||||
// Configure window appearance for macOS
|
||||
if let windowScene = view.window?.windowScene {
|
||||
windowScene.title = "Rollkoffer Simulator"
|
||||
|
||||
// Set window style
|
||||
if let titlebar = windowScene.titlebar {
|
||||
titlebar.titleVisibility = .visible
|
||||
titlebar.toolbarStyle = .unified
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enable keyboard input
|
||||
override var canBecomeFirstResponder: Bool {
|
||||
return true
|
||||
}
|
||||
#endif
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
return .all
|
||||
#else
|
||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||
return .portrait
|
||||
} else {
|
||||
return .all
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
override var prefersStatusBarHidden: Bool {
|
||||
|
||||
@@ -50,5 +50,9 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>13.0</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright 2024 Ingo K. All rights reserved.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -381,6 +381,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
@@ -393,9 +394,12 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.ingok.RollkofferSimulator;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
@@ -409,6 +413,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
@@ -421,9 +426,12 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.ingok.RollkofferSimulator;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
||||
@@ -223,6 +223,27 @@ class GameOverScene: SKScene {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Keyboard Handling (macOS)
|
||||
#if targetEnvironment(macCatalyst)
|
||||
override var canBecomeFirstResponder: Bool { true }
|
||||
|
||||
override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
|
||||
guard let key = presses.first?.key else {
|
||||
super.pressesBegan(presses, with: event)
|
||||
return
|
||||
}
|
||||
|
||||
switch key.keyCode {
|
||||
case .keyboardSpacebar, .keyboardReturnOrEnter:
|
||||
retryGame()
|
||||
case .keyboardEscape:
|
||||
returnToMenu()
|
||||
default:
|
||||
super.pressesBegan(presses, with: event)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private func retryGame() {
|
||||
let pressDown = SKAction.scale(to: 0.9, duration: 0.1)
|
||||
let pressUp = SKAction.scale(to: 1.0, duration: 0.1)
|
||||
|
||||
@@ -318,6 +318,29 @@ class GameScene: SKScene {
|
||||
isDragging = false
|
||||
}
|
||||
|
||||
// MARK: - Keyboard Handling (macOS)
|
||||
#if targetEnvironment(macCatalyst)
|
||||
override var canBecomeFirstResponder: Bool { true }
|
||||
|
||||
override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
|
||||
guard let key = presses.first?.key else {
|
||||
super.pressesBegan(presses, with: event)
|
||||
return
|
||||
}
|
||||
|
||||
switch key.keyCode {
|
||||
case .keyboardEscape:
|
||||
togglePause()
|
||||
case .keyboardSpacebar:
|
||||
if gameState.currentState == .paused {
|
||||
resumeGame()
|
||||
}
|
||||
default:
|
||||
super.pressesBegan(presses, with: event)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - Pause Handling
|
||||
private func togglePause() {
|
||||
if gameState.currentState == .playing {
|
||||
|
||||
@@ -245,6 +245,25 @@ class MenuScene: SKScene {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Keyboard Handling (macOS)
|
||||
#if targetEnvironment(macCatalyst)
|
||||
override var canBecomeFirstResponder: Bool { true }
|
||||
|
||||
override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
|
||||
guard let key = presses.first?.key else {
|
||||
super.pressesBegan(presses, with: event)
|
||||
return
|
||||
}
|
||||
|
||||
switch key.keyCode {
|
||||
case .keyboardSpacebar, .keyboardReturnOrEnter:
|
||||
startGame()
|
||||
default:
|
||||
super.pressesBegan(presses, with: event)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private func startGame() {
|
||||
// Button press effect
|
||||
let pressDown = SKAction.scale(to: 0.9, duration: 0.1)
|
||||
|
||||
@@ -280,6 +280,27 @@ class VictoryScene: SKScene {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Keyboard Handling (macOS)
|
||||
#if targetEnvironment(macCatalyst)
|
||||
override var canBecomeFirstResponder: Bool { true }
|
||||
|
||||
override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
|
||||
guard let key = presses.first?.key else {
|
||||
super.pressesBegan(presses, with: event)
|
||||
return
|
||||
}
|
||||
|
||||
switch key.keyCode {
|
||||
case .keyboardSpacebar, .keyboardReturnOrEnter:
|
||||
playAgain()
|
||||
case .keyboardEscape:
|
||||
returnToMenu()
|
||||
default:
|
||||
super.pressesBegan(presses, with: event)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private func playAgain() {
|
||||
let pressDown = SKAction.scale(to: 0.9, duration: 0.1)
|
||||
let pressUp = SKAction.scale(to: 1.0, duration: 0.1)
|
||||
|
||||
@@ -26,7 +26,8 @@ class SettingsManager {
|
||||
return [
|
||||
'viewer_display' => [
|
||||
'enabled' => true,
|
||||
'min_viewers' => 1
|
||||
'min_viewers' => 1,
|
||||
'update_interval' => 5 // Sekunden
|
||||
],
|
||||
'video_mode' => [
|
||||
'play_in_player' => true,
|
||||
@@ -36,6 +37,52 @@ class SettingsManager {
|
||||
'default_speed' => 1,
|
||||
'available_speeds' => [1, 10, 100]
|
||||
],
|
||||
// Punkt 2: UI-Anzeige Features
|
||||
'ui_display' => [
|
||||
'show_recommendation_banner' => true,
|
||||
'show_qr_code' => true,
|
||||
'show_social_media' => true,
|
||||
'show_patrouille_suisse' => true
|
||||
],
|
||||
// Punkt 3: Zoom & Timelapse
|
||||
'zoom_timelapse' => [
|
||||
'show_zoom_controls' => true,
|
||||
'max_zoom_level' => 4.0,
|
||||
'timelapse_reverse_enabled' => true
|
||||
],
|
||||
// Punkt 5: Content Management
|
||||
'content' => [
|
||||
'guestbook_enabled' => true,
|
||||
'gallery_enabled' => true,
|
||||
'ai_events_enabled' => true,
|
||||
'max_guestbook_entries' => 50
|
||||
],
|
||||
// Punkt 6: Technische Settings
|
||||
'technical' => [
|
||||
'viewer_update_interval' => 5, // Sekunden
|
||||
'session_timeout' => 30 // Sekunden
|
||||
],
|
||||
// Punkt 7: Theme & Design
|
||||
'theme' => [
|
||||
'default_theme' => 'theme-legacy',
|
||||
'show_theme_switcher' => false
|
||||
],
|
||||
// Punkt 8: SEO & Meta
|
||||
'seo' => [
|
||||
'custom_title' => '',
|
||||
'meta_description' => '',
|
||||
'meta_keywords' => ''
|
||||
],
|
||||
// Weather Widget
|
||||
'weather' => [
|
||||
'enabled' => true,
|
||||
'api_key' => '',
|
||||
'location' => 'Oberdürnten,CH',
|
||||
'lat' => '47.2833',
|
||||
'lon' => '8.7167',
|
||||
'update_interval' => 5, // Minuten
|
||||
'units' => 'metric' // metric (Celsius) oder imperial (Fahrenheit)
|
||||
],
|
||||
'last_updated' => null,
|
||||
'updated_by' => null
|
||||
];
|
||||
@@ -123,4 +170,111 @@ class SettingsManager {
|
||||
public function shouldAllowDownload() {
|
||||
return $this->get('video_mode.allow_download') === true;
|
||||
}
|
||||
|
||||
// UI Display Helper
|
||||
public function shouldShowRecommendationBanner() {
|
||||
return $this->get('ui_display.show_recommendation_banner') === true;
|
||||
}
|
||||
|
||||
public function shouldShowQRCode() {
|
||||
return $this->get('ui_display.show_qr_code') === true;
|
||||
}
|
||||
|
||||
public function shouldShowSocialMedia() {
|
||||
return $this->get('ui_display.show_social_media') === true;
|
||||
}
|
||||
|
||||
public function shouldShowPatrouillesuisse() {
|
||||
return $this->get('ui_display.show_patrouille_suisse') === true;
|
||||
}
|
||||
|
||||
// Content Management Helper
|
||||
public function isGuestbookEnabled() {
|
||||
return $this->get('content.guestbook_enabled') === true;
|
||||
}
|
||||
|
||||
public function isGalleryEnabled() {
|
||||
return $this->get('content.gallery_enabled') === true;
|
||||
}
|
||||
|
||||
public function isAIEventsEnabled() {
|
||||
return $this->get('content.ai_events_enabled') === true;
|
||||
}
|
||||
|
||||
public function getMaxGuestbookEntries() {
|
||||
return $this->get('content.max_guestbook_entries') ?? 50;
|
||||
}
|
||||
|
||||
// Theme Helper
|
||||
public function getDefaultTheme() {
|
||||
return $this->get('theme.default_theme') ?? 'theme-legacy';
|
||||
}
|
||||
|
||||
public function shouldShowThemeSwitcher() {
|
||||
return $this->get('theme.show_theme_switcher') === true;
|
||||
}
|
||||
|
||||
// Technical Helper
|
||||
public function getViewerUpdateInterval() {
|
||||
return $this->get('technical.viewer_update_interval') ?? 5;
|
||||
}
|
||||
|
||||
public function getSessionTimeout() {
|
||||
return $this->get('technical.session_timeout') ?? 30;
|
||||
}
|
||||
|
||||
// Zoom & Timelapse Helper
|
||||
public function shouldShowZoomControls() {
|
||||
return $this->get('zoom_timelapse.show_zoom_controls') === true;
|
||||
}
|
||||
|
||||
public function getMaxZoomLevel() {
|
||||
return $this->get('zoom_timelapse.max_zoom_level') ?? 4.0;
|
||||
}
|
||||
|
||||
public function isTimelapseReverseEnabled() {
|
||||
return $this->get('zoom_timelapse.timelapse_reverse_enabled') === true;
|
||||
}
|
||||
|
||||
// SEO Helper
|
||||
public function getCustomTitle() {
|
||||
$title = $this->get('seo.custom_title');
|
||||
return !empty($title) ? $title : null;
|
||||
}
|
||||
|
||||
public function getMetaDescription() {
|
||||
return $this->get('seo.meta_description') ?? '';
|
||||
}
|
||||
|
||||
public function getMetaKeywords() {
|
||||
return $this->get('seo.meta_keywords') ?? '';
|
||||
}
|
||||
|
||||
// Weather Helper
|
||||
public function isWeatherEnabled() {
|
||||
return $this->get('weather.enabled') === true;
|
||||
}
|
||||
|
||||
public function getWeatherApiKey() {
|
||||
return $this->get('weather.api_key') ?? '';
|
||||
}
|
||||
|
||||
public function getWeatherLocation() {
|
||||
return $this->get('weather.location') ?? 'Oberdürnten,CH';
|
||||
}
|
||||
|
||||
public function getWeatherCoords() {
|
||||
return [
|
||||
'lat' => $this->get('weather.lat') ?? '47.2833',
|
||||
'lon' => $this->get('weather.lon') ?? '8.7167'
|
||||
];
|
||||
}
|
||||
|
||||
public function getWeatherUpdateInterval() {
|
||||
return $this->get('weather.update_interval') ?? 5;
|
||||
}
|
||||
|
||||
public function getWeatherUnits() {
|
||||
return $this->get('weather.units') ?? 'metric';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
/**
|
||||
* WeatherManager - Holt und cached Wetterdaten von OpenWeatherMap
|
||||
*/
|
||||
class WeatherManager {
|
||||
private $settingsManager;
|
||||
private $cacheFile = 'weather_cache.json';
|
||||
private $cacheTime = 300; // 5 Minuten in Sekunden
|
||||
|
||||
public function __construct($settingsManager) {
|
||||
$this->settingsManager = $settingsManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt aktuelle Wetterdaten (cached)
|
||||
*/
|
||||
public function getCurrentWeather() {
|
||||
// Prüfe ob Weather aktiviert ist
|
||||
if (!$this->settingsManager->isWeatherEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Prüfe API Key
|
||||
$apiKey = $this->settingsManager->getWeatherApiKey();
|
||||
if (empty($apiKey)) {
|
||||
return ['error' => 'API Key fehlt'];
|
||||
}
|
||||
|
||||
// Prüfe Cache
|
||||
$cached = $this->getCache();
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
// Hole frische Daten von API
|
||||
$coords = $this->settingsManager->getWeatherCoords();
|
||||
$units = $this->settingsManager->getWeatherUnits();
|
||||
|
||||
$url = "https://api.openweathermap.org/data/2.5/weather?lat={$coords['lat']}&lon={$coords['lon']}&units={$units}&appid={$apiKey}&lang=de";
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200 || !$response) {
|
||||
return ['error' => 'API Fehler'];
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
if (!$data || !isset($data['main'])) {
|
||||
return ['error' => 'Ungültige API Antwort'];
|
||||
}
|
||||
|
||||
// Formatiere Daten
|
||||
$weather = [
|
||||
'temp' => round($data['main']['temp'], 1),
|
||||
'feels_like' => round($data['main']['feels_like'], 1),
|
||||
'humidity' => $data['main']['humidity'],
|
||||
'pressure' => $data['main']['pressure'],
|
||||
'wind_speed' => round($data['wind']['speed'] * 3.6, 1), // m/s -> km/h
|
||||
'wind_deg' => $data['wind']['deg'] ?? 0,
|
||||
'wind_direction' => $this->getWindDirection($data['wind']['deg'] ?? 0),
|
||||
'clouds' => $data['clouds']['all'] ?? 0,
|
||||
'description' => ucfirst($data['weather'][0]['description'] ?? 'Unbekannt'),
|
||||
'icon' => $data['weather'][0]['icon'] ?? '01d',
|
||||
'rain_1h' => $data['rain']['1h'] ?? 0,
|
||||
'snow_1h' => $data['snow']['1h'] ?? 0,
|
||||
'location' => $data['name'] ?? $this->settingsManager->getWeatherLocation(),
|
||||
'timestamp' => time()
|
||||
];
|
||||
|
||||
// Cache speichern
|
||||
$this->saveCache($weather);
|
||||
|
||||
return $weather;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wandelt Windrichtung (Grad) in Kompassrichtung um
|
||||
*/
|
||||
private function getWindDirection($deg) {
|
||||
$directions = ['N', 'NNO', 'NO', 'ONO', 'O', 'OSO', 'SO', 'SSO', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'];
|
||||
$index = round($deg / 22.5) % 16;
|
||||
return $directions[$index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt Daten aus Cache (wenn noch gültig)
|
||||
*/
|
||||
private function getCache() {
|
||||
if (!file_exists($this->cacheFile)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$content = file_get_contents($this->cacheFile);
|
||||
$data = json_decode($content, true);
|
||||
|
||||
if (!$data || !isset($data['timestamp'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Update-Intervall aus Settings holen (in Minuten)
|
||||
$updateInterval = $this->settingsManager->getWeatherUpdateInterval() * 60; // Minuten -> Sekunden
|
||||
|
||||
// Prüfe ob Cache noch gültig
|
||||
if (time() - $data['timestamp'] < $updateInterval) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert Daten im Cache
|
||||
*/
|
||||
private function saveCache($data) {
|
||||
$json = json_encode($data, JSON_PRETTY_PRINT);
|
||||
file_put_contents($this->cacheFile, $json, LOCK_EX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt Wetter-Icon-Emoji zurück
|
||||
*/
|
||||
public function getWeatherEmoji($iconCode) {
|
||||
$map = [
|
||||
'01d' => '☀️', '01n' => '🌙',
|
||||
'02d' => '⛅', '02n' => '☁️',
|
||||
'03d' => '☁️', '03n' => '☁️',
|
||||
'04d' => '☁️', '04n' => '☁️',
|
||||
'09d' => '🌧️', '09n' => '🌧️',
|
||||
'10d' => '🌦️', '10n' => '🌧️',
|
||||
'11d' => '⛈️', '11n' => '⛈️',
|
||||
'13d' => '❄️', '13n' => '❄️',
|
||||
'50d' => '🌫️', '50n' => '🌫️'
|
||||
];
|
||||
return $map[$iconCode] ?? '🌤️';
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX Handler für Wetter-Updates
|
||||
*/
|
||||
public function handleAjax() {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') return;
|
||||
if (!isset($_GET['weather_action'])) return;
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_GET['weather_action'] === 'get') {
|
||||
$weather = $this->getCurrentWeather();
|
||||
echo json_encode(['success' => true, 'data' => $weather]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
// Clear PHP OPcache
|
||||
if (function_exists('opcache_reset')) {
|
||||
opcache_reset();
|
||||
echo "OPcache cleared successfully!\n";
|
||||
} else {
|
||||
echo "OPcache not available\n";
|
||||
}
|
||||
|
||||
// Clear realpath cache
|
||||
clearstatcache(true);
|
||||
echo "Realpath cache cleared!\n";
|
||||
|
||||
echo "\nNow reload the page with CTRL+F5 (hard refresh)\n";
|
||||
?>
|
||||
+1181
-76
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* Video Zoom & Pan Controller
|
||||
* - Zoom für alle Video-Modi (Live, Timelapse, Tagesvideo)
|
||||
* - Pan-Funktion: Mit Maus den gezoomten Bereich verschieben
|
||||
* Zoomt auf Wrapper-Layer statt direkt auf Video-Elemente
|
||||
*/
|
||||
(() => {
|
||||
const config = window.zoomConfig || {};
|
||||
@@ -11,60 +10,75 @@
|
||||
let panX = 0;
|
||||
let panY = 0;
|
||||
let isDragging = false;
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
let lastX = 0;
|
||||
let lastY = 0;
|
||||
|
||||
const minZoom = Number(config.minZoom || 1);
|
||||
const maxZoom = Number(config.maxZoom || 4);
|
||||
const defaultZoom = Number(config.defaultZoom || 1);
|
||||
|
||||
const slider = document.getElementById('zoom-range');
|
||||
const valueEl = document.getElementById('zoom-value');
|
||||
|
||||
// Finde das aktuell aktive Video-Element
|
||||
function getActiveTarget() {
|
||||
const webcam = document.getElementById('webcam-player');
|
||||
const timelapse = document.getElementById('timelapse-image');
|
||||
const daily = document.getElementById('daily-video');
|
||||
const timelapseViewer = document.getElementById('timelapse-viewer');
|
||||
// Wrapper-IDs für jeden Modus
|
||||
const wrapperIds = ['live-video-wrapper', 'timelapse-wrapper', 'daily-video-wrapper'];
|
||||
|
||||
// Finde den aktuell sichtbaren Wrapper
|
||||
function getActiveWrapper() {
|
||||
// Prüfe daily-video-player
|
||||
const dailyPlayer = document.getElementById('daily-video-player');
|
||||
|
||||
// Prüfe welches Element sichtbar ist
|
||||
if (dailyPlayer && dailyPlayer.style.display !== 'none' && daily) {
|
||||
return daily;
|
||||
}
|
||||
if (timelapseViewer && timelapseViewer.style.display !== 'none' && timelapse) {
|
||||
return timelapse;
|
||||
}
|
||||
if (webcam) {
|
||||
return webcam;
|
||||
}
|
||||
return null;
|
||||
if (dailyPlayer && dailyPlayer.style.display !== 'none') {
|
||||
return document.getElementById('daily-video-wrapper');
|
||||
}
|
||||
|
||||
// Wende Zoom und Pan auf das aktive Element an
|
||||
// Prüfe timelapse-viewer
|
||||
const timelapseViewer = document.getElementById('timelapse-viewer');
|
||||
if (timelapseViewer && timelapseViewer.style.display !== 'none') {
|
||||
return document.getElementById('timelapse-wrapper');
|
||||
}
|
||||
|
||||
// Fallback: Live-Video
|
||||
return document.getElementById('live-video-wrapper');
|
||||
}
|
||||
|
||||
// Wende Transform auf ALLE Wrapper an (damit beim Wechsel der Zoom erhalten bleibt)
|
||||
function applyTransform() {
|
||||
const target = getActiveTarget();
|
||||
if (!target) return;
|
||||
|
||||
// Bei Zoom 1x: Kein Pan erlaubt
|
||||
// Bei Zoom 1x: Kein Pan
|
||||
if (currentZoom <= 1) {
|
||||
panX = 0;
|
||||
panY = 0;
|
||||
}
|
||||
|
||||
// Begrenzen der Pan-Werte basierend auf Zoom
|
||||
const maxPan = (currentZoom - 1) * 50; // Prozent
|
||||
// Pan begrenzen basierend auf Zoom
|
||||
const maxPan = (currentZoom - 1) * 50;
|
||||
panX = Math.max(-maxPan, Math.min(maxPan, panX));
|
||||
panY = Math.max(-maxPan, Math.min(maxPan, panY));
|
||||
|
||||
target.style.transform = `scale(${currentZoom}) translate(${panX}%, ${panY}%)`;
|
||||
target.style.transformOrigin = 'center center';
|
||||
target.style.transition = isDragging ? 'none' : 'transform 0.2s ease';
|
||||
// Transform auf alle Wrapper anwenden
|
||||
wrapperIds.forEach(id => {
|
||||
const wrapper = document.getElementById(id);
|
||||
if (wrapper) {
|
||||
wrapper.style.transform = `scale(${currentZoom}) translate(${panX}%, ${panY}%)`;
|
||||
wrapper.style.transition = isDragging ? 'none' : 'transform 0.15s ease-out';
|
||||
}
|
||||
});
|
||||
|
||||
// Update UI
|
||||
// UI Update
|
||||
if (valueEl) valueEl.textContent = `${currentZoom.toFixed(1)}x`;
|
||||
if (slider) slider.value = currentZoom;
|
||||
|
||||
// Cursor Update
|
||||
updateCursor();
|
||||
}
|
||||
|
||||
function updateCursor() {
|
||||
const container = document.querySelector('.video-container');
|
||||
if (container) {
|
||||
if (currentZoom > 1) {
|
||||
container.classList.add('zoomed');
|
||||
} else {
|
||||
container.classList.remove('zoomed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Zoom setzen
|
||||
@@ -91,57 +105,68 @@
|
||||
const container = document.querySelector('.video-container');
|
||||
if (!container) return;
|
||||
|
||||
// Mousedown - Start dragging
|
||||
container.addEventListener('mousedown', (e) => {
|
||||
if (currentZoom <= 1) return;
|
||||
// Ignoriere Klicks auf Controls
|
||||
if (e.target.closest('.zoom-controls, button, a')) return;
|
||||
|
||||
isDragging = true;
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
container.style.cursor = 'grabbing';
|
||||
lastX = e.clientX;
|
||||
lastY = e.clientY;
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// Mousemove - Dragging
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!isDragging) return;
|
||||
|
||||
const dx = (e.clientX - startX) / 5; // Sensitivität anpassen
|
||||
const dy = (e.clientY - startY) / 5;
|
||||
const deltaX = e.clientX - lastX;
|
||||
const deltaY = e.clientY - lastY;
|
||||
|
||||
panX += dx / currentZoom;
|
||||
panY += dy / currentZoom;
|
||||
// Sensitivität basierend auf Zoom
|
||||
const sensitivity = 0.15 / currentZoom;
|
||||
panX += deltaX * sensitivity;
|
||||
panY += deltaY * sensitivity;
|
||||
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
lastX = e.clientX;
|
||||
lastY = e.clientY;
|
||||
|
||||
applyTransform();
|
||||
});
|
||||
|
||||
// Mouseup - Stop dragging
|
||||
document.addEventListener('mouseup', () => {
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
const container = document.querySelector('.video-container');
|
||||
if (container) container.style.cursor = currentZoom > 1 ? 'grab' : 'default';
|
||||
}
|
||||
});
|
||||
|
||||
// Mouse leave
|
||||
document.addEventListener('mouseleave', () => {
|
||||
isDragging = false;
|
||||
});
|
||||
|
||||
// Touch Events für Mobile
|
||||
container.addEventListener('touchstart', (e) => {
|
||||
if (currentZoom <= 1 || e.touches.length !== 1) return;
|
||||
if (e.target.closest('.zoom-controls, button, a')) return;
|
||||
|
||||
isDragging = true;
|
||||
startX = e.touches[0].clientX;
|
||||
startY = e.touches[0].clientY;
|
||||
lastX = e.touches[0].clientX;
|
||||
lastY = e.touches[0].clientY;
|
||||
}, { passive: true });
|
||||
|
||||
container.addEventListener('touchmove', (e) => {
|
||||
if (!isDragging || e.touches.length !== 1) return;
|
||||
|
||||
const dx = (e.touches[0].clientX - startX) / 5;
|
||||
const dy = (e.touches[0].clientY - startY) / 5;
|
||||
const deltaX = e.touches[0].clientX - lastX;
|
||||
const deltaY = e.touches[0].clientY - lastY;
|
||||
|
||||
panX += dx / currentZoom;
|
||||
panY += dy / currentZoom;
|
||||
const sensitivity = 0.15 / currentZoom;
|
||||
panX += deltaX * sensitivity;
|
||||
panY += deltaY * sensitivity;
|
||||
|
||||
startX = e.touches[0].clientX;
|
||||
startY = e.touches[0].clientY;
|
||||
lastX = e.touches[0].clientX;
|
||||
lastY = e.touches[0].clientY;
|
||||
|
||||
applyTransform();
|
||||
}, { passive: true });
|
||||
@@ -150,25 +175,28 @@
|
||||
isDragging = false;
|
||||
});
|
||||
|
||||
// Cursor anpassen bei Zoom
|
||||
container.style.cursor = 'default';
|
||||
// Doppelklick zum Zurücksetzen
|
||||
container.addEventListener('dblclick', (e) => {
|
||||
if (e.target.closest('.zoom-controls, button, a')) return;
|
||||
resetZoom();
|
||||
});
|
||||
}
|
||||
|
||||
// Slider Events
|
||||
// Slider Setup
|
||||
function setupSlider() {
|
||||
if (!slider) return;
|
||||
|
||||
slider.min = minZoom;
|
||||
slider.max = maxZoom;
|
||||
slider.step = 0.5;
|
||||
slider.value = defaultZoom;
|
||||
slider.value = 1;
|
||||
|
||||
slider.addEventListener('input', (e) => {
|
||||
setZoom(Number(e.target.value));
|
||||
});
|
||||
}
|
||||
|
||||
// Globale Funktionen für Buttons
|
||||
// Globale Funktionen
|
||||
window.adjustZoom = adjustZoom;
|
||||
window.resetZoom = resetZoom;
|
||||
window.setZoom = setZoom;
|
||||
@@ -177,31 +205,11 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
setupSlider();
|
||||
setupPanEvents();
|
||||
currentZoom = defaultZoom;
|
||||
|
||||
// Warte kurz, damit Video-Elemente geladen sind
|
||||
setTimeout(() => {
|
||||
// Initial State
|
||||
currentZoom = 1;
|
||||
applyTransform();
|
||||
}, 500);
|
||||
|
||||
// Update Cursor bei Zoom-Änderung
|
||||
const container = document.querySelector('.video-container');
|
||||
if (container) {
|
||||
const observer = new MutationObserver(() => {
|
||||
container.style.cursor = currentZoom > 1 ? 'grab' : 'default';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Bei Moduswechsel Pan zurücksetzen
|
||||
window.addEventListener('click', (e) => {
|
||||
if (e.target.id === 'timelapse-button' ||
|
||||
e.target.closest('#timelapse-button') ||
|
||||
e.target.id === 'dvp-back-live' ||
|
||||
e.target.closest('.play-link')) {
|
||||
panX = 0;
|
||||
panY = 0;
|
||||
setTimeout(applyTransform, 100);
|
||||
}
|
||||
console.log('Video Zoom & Pan initialized');
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"viewer_display": {
|
||||
"enabled": true,
|
||||
"min_viewers": 1
|
||||
"min_viewers": 1,
|
||||
"update_interval": 5
|
||||
},
|
||||
"video_mode": {
|
||||
"play_in_player": true,
|
||||
@@ -9,7 +10,50 @@
|
||||
},
|
||||
"timelapse": {
|
||||
"default_speed": 1,
|
||||
"available_speeds": [1, 10, 100]
|
||||
"available_speeds": [
|
||||
1,
|
||||
10,
|
||||
100
|
||||
]
|
||||
},
|
||||
"ui_display": {
|
||||
"show_recommendation_banner": true,
|
||||
"show_qr_code": true,
|
||||
"show_social_media": true,
|
||||
"show_patrouille_suisse": true
|
||||
},
|
||||
"zoom_timelapse": {
|
||||
"show_zoom_controls": true,
|
||||
"max_zoom_level": 4.0,
|
||||
"timelapse_reverse_enabled": true
|
||||
},
|
||||
"content": {
|
||||
"guestbook_enabled": true,
|
||||
"gallery_enabled": true,
|
||||
"ai_events_enabled": true,
|
||||
"max_guestbook_entries": 50
|
||||
},
|
||||
"technical": {
|
||||
"viewer_update_interval": 5,
|
||||
"session_timeout": 30
|
||||
},
|
||||
"theme": {
|
||||
"default_theme": "theme-legacy",
|
||||
"show_theme_switcher": false
|
||||
},
|
||||
"seo": {
|
||||
"custom_title": "",
|
||||
"meta_description": "",
|
||||
"meta_keywords": ""
|
||||
},
|
||||
"weather": {
|
||||
"enabled": true,
|
||||
"api_key": "",
|
||||
"location": "Oberdürnten,CH",
|
||||
"lat": "47.2833",
|
||||
"lon": "8.7167",
|
||||
"update_interval": 5,
|
||||
"units": "metric"
|
||||
},
|
||||
"last_updated": null,
|
||||
"updated_by": null
|
||||
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
Erstelle ein Word-Dokument: "Power BI Schulungshandbuch für HR" mit Schritt-für-Schritt-Anleitungen.
|
||||
|
||||
ZIELGRUPPE:
|
||||
- 3-4 HR-Mitarbeiterinnen, Schweiz
|
||||
- Excel-Kenntnisse: Basis + SVERWEIS
|
||||
- Technikaffinität: 5-6/10
|
||||
- Keine Power BI Vorkenntnisse
|
||||
|
||||
DATENQUELLEN DER TEILNEHMER:
|
||||
- SAP HCM/HRM (alle Infotypen, besonders PA0001, PA0002, PA0008, PA2001)
|
||||
- Rexx HR-System (Stellenplan, Pulsumfrage, MA-Zufriedenheit)
|
||||
- Excel/CSV (Kununu-Score, Refline/Time-to-hire)
|
||||
|
||||
KPIs DIE ABGEBILDET WERDEN SOLLEN:
|
||||
- Headcount/FTE (monatlich)
|
||||
- Fluktuation (monatlich)
|
||||
- Krankenquote gesamt + ohne Langzeitkrankheiten >30 Tage (Quartal)
|
||||
- Überstunden (Quartal)
|
||||
- Produktivstunden (wöchentlich)
|
||||
- Ferientage/GLZ-Saldi (jährlich)
|
||||
- Stellenplan Soll vs Ist (monatlich, aus Rexx)
|
||||
- Lohnkosten (monatlich)
|
||||
- Time to hire (Quartal)
|
||||
- Kununu Score (monatlich)
|
||||
- Pulsumfrage (Quartal, aus Rexx)
|
||||
- MA-Zufriedenheitsumfrage (jährlich, aus Rexx)
|
||||
|
||||
ZIELGRUPPEN DER REPORTS:
|
||||
- Geschäftsleitung
|
||||
- Verwaltungsrat
|
||||
- Finanzbuchhaltung
|
||||
- Abteilungsleiter
|
||||
|
||||
STRUKTUR DES DOKUMENTS:
|
||||
|
||||
1. MODUL 1: GRUNDLAGEN & DATENIMPORT
|
||||
1.1 Power BI Desktop installieren und starten
|
||||
- Wo herunterladen, Installation, erster Start
|
||||
1.2 Oberfläche kennenlernen
|
||||
- Berichtsansicht, Datenansicht, Modellansicht erklären
|
||||
- Wo findet man was (Menüband, Felder-Bereich, Visualisierungen)
|
||||
1.3 Excel-Datei importieren
|
||||
- Schritt-für-Schritt: Daten abrufen → Excel → Datei wählen → Navigator → Laden
|
||||
- Häufige Probleme und Lösungen
|
||||
1.4 CSV importieren
|
||||
- Unterschiede zu Excel, Encoding-Probleme Schweiz (Umlaute)
|
||||
1.5 SAP-Export importieren
|
||||
- Typische SAP-Exportformate verarbeiten
|
||||
- Spaltenüberschriften aus erster Zeile
|
||||
|
||||
2. MODUL 2: POWER QUERY EDITOR
|
||||
2.1 Power Query öffnen
|
||||
- Daten transformieren → Button finden
|
||||
2.2 Erste Zeile als Header verwenden
|
||||
- Schritt-für-Schritt mit Menüpfad
|
||||
2.3 Datentypen ändern
|
||||
- Datum, Zahl, Text erkennen und korrigieren
|
||||
- Schweizer Datumsformat beachten
|
||||
2.4 Spalten entfernen/behalten
|
||||
- Nur relevante Spalten behalten
|
||||
2.5 Zeilen filtern
|
||||
- Beispiel: Nur aktive Mitarbeiter, nur bestimmter Zeitraum
|
||||
2.6 Werte ersetzen
|
||||
- null durch 0 ersetzen, Codes durch Klartext
|
||||
2.7 Spalten teilen/zusammenführen
|
||||
2.8 Berechnete Spalte hinzufügen
|
||||
2.9 Schliessen und Laden
|
||||
- Unterschied: Laden vs. Laden in
|
||||
|
||||
3. MODUL 3: DATENMODELL
|
||||
3.1 Zur Modellansicht wechseln
|
||||
3.2 Beziehungen verstehen
|
||||
- 1:n, 1:1 erklären
|
||||
- Warum Beziehungen wichtig sind
|
||||
3.3 Beziehung erstellen
|
||||
- Drag & Drop zwischen Tabellen
|
||||
- Beziehung bearbeiten (Kardinalität, Kreuzfilterrichtung)
|
||||
3.4 Datumstabelle erstellen
|
||||
- Warum eigene Datumstabelle nötig
|
||||
- DAX-Formel zum Erstellen:
|
||||
Datum = ADDCOLUMNS(CALENDAR(DATE(2020,1,1), TODAY()), "Jahr", YEAR([Date]), "Monat", MONTH([Date]), "MonatName", FORMAT([Date],"MMMM"), "Quartal", "Q" & QUARTER([Date]), "KW", WEEKNUM([Date]))
|
||||
- Als Datumstabelle markieren (Menüpfad)
|
||||
3.5 PERNR als Schlüssel
|
||||
- Personalnummer verbindet alle SAP-Tabellen
|
||||
|
||||
4. MODUL 4: DAX MEASURES
|
||||
4.1 Was ist ein Measure vs. berechnete Spalte
|
||||
4.2 Neues Measure erstellen
|
||||
- Menüpfad: Modellierung → Neues Measure
|
||||
4.3 Basis-Measures für HR:
|
||||
|
||||
Headcount:
|
||||
Headcount = COUNTROWS(Mitarbeiter)
|
||||
|
||||
FTE:
|
||||
FTE = SUMX(Mitarbeiter, Mitarbeiter[Beschäftigungsgrad]/100)
|
||||
|
||||
Krankheitstage:
|
||||
Krankheitstage = SUM(Abwesenheiten[Kalendertage])
|
||||
|
||||
Sollarbeitstage:
|
||||
Sollarbeitstage = [Headcount] * 21
|
||||
|
||||
Krankenquote:
|
||||
Krankenquote = DIVIDE([Krankheitstage], [Sollarbeitstage], 0)
|
||||
|
||||
Krankenquote ohne Langzeit (>30 Tage):
|
||||
Krankenquote_ohne_LZ =
|
||||
VAR KrankheitstageKurz = CALCULATE([Krankheitstage], FILTER(Abwesenheiten, Abwesenheiten[Kalendertage] <= 30))
|
||||
RETURN DIVIDE(KrankheitstageKurz, [Sollarbeitstage], 0)
|
||||
|
||||
Austritte:
|
||||
Austritte = CALCULATE(COUNTROWS(Mitarbeiter), Mitarbeiter[Austritt] <> BLANK())
|
||||
|
||||
Durchschnittlicher Headcount:
|
||||
Avg_Headcount = AVERAGEX(VALUES(Datum[Monat]), [Headcount])
|
||||
|
||||
Fluktuation:
|
||||
Fluktuation = DIVIDE([Austritte], [Avg_Headcount], 0) * 100
|
||||
|
||||
4.4 Zeitintelligenz-Measures:
|
||||
|
||||
Vorjahreswert:
|
||||
Headcount_VJ = CALCULATE([Headcount], SAMEPERIODLASTYEAR(Datum[Date]))
|
||||
|
||||
Vormonat:
|
||||
Headcount_VM = CALCULATE([Headcount], PREVIOUSMONTH(Datum[Date]))
|
||||
|
||||
Year-to-Date:
|
||||
Headcount_YTD = TOTALYTD([Headcount], Datum[Date])
|
||||
|
||||
Delta zum Vorjahr:
|
||||
Delta_VJ = [Headcount] - [Headcount_VJ]
|
||||
|
||||
Delta Prozent:
|
||||
Delta_VJ_Proz = DIVIDE([Delta_VJ], [Headcount_VJ], 0)
|
||||
|
||||
4.5 Measures formatieren
|
||||
- Prozent, Dezimalstellen, Währung einstellen
|
||||
|
||||
5. MODUL 5: VISUALISIERUNGEN
|
||||
5.1 Visualisierungstypen und wann verwenden:
|
||||
- Karte/Card: Einzelne KPI-Zahl (Headcount, Krankenquote)
|
||||
- Balkendiagramm: Vergleiche (Abteilungen, Monate)
|
||||
- Liniendiagramm: Zeitverläufe (Headcount über 12 Monate)
|
||||
- Ringdiagramm: Anteile (Absenzen nach Typ)
|
||||
- Tachometer: Ziel vs Ist (Stellenplan-Erfüllung)
|
||||
- Tabelle/Matrix: Details mit Drill-down
|
||||
|
||||
5.2 Erste Visualisierung erstellen
|
||||
- Schritt-für-Schritt: Visualisierung wählen → Felder reinziehen
|
||||
5.3 Visualisierung formatieren
|
||||
- Titel, Farben, Schriftgrössen
|
||||
5.4 Filter hinzufügen
|
||||
- Visualfilter, Seitenfilter, Berichtsfilter
|
||||
5.5 Slicer erstellen
|
||||
- Zeitraum-Auswahl, Abteilungs-Auswahl
|
||||
5.6 Bedingte Formatierung
|
||||
- Rot/Grün je nach Wert (Ampel-Logik)
|
||||
|
||||
6. MODUL 6: DASHBOARD BAUEN
|
||||
6.1 Dashboard-Layout planen
|
||||
- F-Muster: Wichtigstes oben links
|
||||
- Max 6-8 Visualisierungen pro Seite
|
||||
6.2 Seite 1: Management-Übersicht erstellen
|
||||
- KPI-Karten oben: Headcount, Krankenquote, Fluktuation, Stellenplan
|
||||
- Trendlinie Headcount
|
||||
- Absenzquote nach Typ
|
||||
6.3 Seite 2: Detailanalyse erstellen
|
||||
- Matrix mit Drill-down nach Abteilung
|
||||
- Filter für Zeitraum und Kostenstelle
|
||||
6.4 Interaktionen zwischen Visualisierungen
|
||||
- Klick auf Balken filtert andere Visuals
|
||||
- Interaktionen bearbeiten (Menüpfad)
|
||||
6.5 Design-Tipps
|
||||
- Konsistente Farben (Firmen-CI)
|
||||
- Genügend Weissraum
|
||||
- Beschriftungen lesbar
|
||||
|
||||
7. MODUL 7: VERÖFFENTLICHEN & TEILEN
|
||||
7.1 Power BI Service (app.powerbi.com)
|
||||
- Konto erstellen/anmelden
|
||||
- Unterschied Desktop vs Service
|
||||
7.2 Bericht veröffentlichen
|
||||
- Menüpfad: Datei → Veröffentlichen → Arbeitsbereich wählen
|
||||
7.3 Arbeitsbereich einrichten
|
||||
7.4 Dashboard erstellen (aus Bericht)
|
||||
- Visualisierung anheften
|
||||
7.5 Bericht teilen
|
||||
- Link teilen, Zugriff verwalten
|
||||
7.6 Automatische Aktualisierung einrichten
|
||||
- Geplante Aktualisierung (täglich, wöchentlich)
|
||||
- Gateway für lokale Daten (IT einbeziehen)
|
||||
7.7 Row-Level Security (RLS)
|
||||
- Abteilungsleiter sehen nur eigene Daten
|
||||
- Rolle erstellen, DAX-Filter: [Abteilung] = USERPRINCIPALNAME()
|
||||
|
||||
8. TROUBLESHOOTING
|
||||
8.1 Häufige Fehler beim Import
|
||||
- Encoding-Probleme (UTF-8)
|
||||
- Falsches Dezimaltrennzeichen (Punkt vs Komma)
|
||||
- Datum wird als Text erkannt
|
||||
8.2 Häufige DAX-Fehler
|
||||
- Zirkelbezug
|
||||
- Division durch Null (DIVIDE verwenden)
|
||||
- Falscher Filterkontext
|
||||
8.3 Beziehungsprobleme
|
||||
- Mehrdeutige Beziehungen
|
||||
- Fehlende Beziehung
|
||||
8.4 Performance-Probleme
|
||||
- Zu viele Spalten importiert
|
||||
- Berechnete Spalten vs Measures
|
||||
|
||||
9. ANHANG
|
||||
9.1 DAX Cheat Sheet (alle HR-Formeln auf einer Seite)
|
||||
9.2 Checkliste: Neuen Report erstellen
|
||||
9.3 Glossar (Power Query, DAX, Measure, etc.)
|
||||
|
||||
FORMAT-ANWEISUNGEN:
|
||||
- Jeder Schritt nummeriert
|
||||
- Menüpfade in Format: Reiter → Gruppe → Button
|
||||
- DAX-Formeln in Codeblock/Monospace
|
||||
- Tipps und Warnungen hervorheben
|
||||
- Screenshots beschreiben wo sinnvoll: [Screenshot: Beschreibung was zu sehen sein sollte]
|
||||
- Sprache: Deutsch (Schweiz), Du-Form
|
||||
@@ -0,0 +1,556 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de-CH">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Power BI Schulungshandbuch für HR</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--accent: #1f6feb;
|
||||
--accent-soft: #e0f2fe;
|
||||
--text: #0f172a;
|
||||
--muted: #475569;
|
||||
--bg: #f8fafc;
|
||||
--card: #ffffff;
|
||||
--border: #e2e8f0;
|
||||
--warning: #f97316;
|
||||
--success: #16a34a;
|
||||
--code: #0b1020;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
line-height: 1.7;
|
||||
}
|
||||
header {
|
||||
background: linear-gradient(120deg, #e0f2fe 0%, #eef2ff 100%);
|
||||
padding: 40px 24px 24px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
header h1 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
header p {
|
||||
margin: 6px 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
main {
|
||||
max-width: 1050px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
}
|
||||
section {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 26px;
|
||||
margin-bottom: 22px;
|
||||
box-shadow: 0 4px 18px rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
color: #111827;
|
||||
border-bottom: 2px solid var(--border);
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
h3 {
|
||||
margin-bottom: 6px;
|
||||
color: #1e293b;
|
||||
}
|
||||
h4 {
|
||||
margin: 14px 0 6px;
|
||||
color: #1f2937;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.85rem;
|
||||
background: var(--accent-soft);
|
||||
color: #0369a1;
|
||||
margin-left: 8px;
|
||||
}
|
||||
ul, ol {
|
||||
margin: 8px 0 16px 24px;
|
||||
}
|
||||
.callout {
|
||||
border-left: 4px solid var(--accent);
|
||||
background: #eef2ff;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
margin: 12px 0;
|
||||
color: #1e293b;
|
||||
}
|
||||
.warning {
|
||||
border-left-color: var(--warning);
|
||||
background: #fff7ed;
|
||||
}
|
||||
.success {
|
||||
border-left-color: var(--success);
|
||||
background: #ecfdf3;
|
||||
}
|
||||
pre {
|
||||
background: var(--code);
|
||||
color: #e2e8f0;
|
||||
padding: 16px;
|
||||
border-radius: 10px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
code {
|
||||
font-family: "Consolas", "Courier New", monospace;
|
||||
}
|
||||
figure {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
figcaption {
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.grid-two {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
.grid-three {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
.kpi-list li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.checklist li {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.small {
|
||||
font-size: 0.92rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
.flow-box {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
background: #f1f5f9;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Power BI Schulungshandbuch für HR</h1>
|
||||
<p>Für 3–4 HR-Mitarbeiterinnen (Schweiz) mit wenig IT-Kenntnissen und Excel-Basis.</p>
|
||||
<p class="small">Hinweis: Eine Word-Version ist nicht im Repo enthalten (Binary-Dateien werden beim PR-Erstellen nicht unterstützt).</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section>
|
||||
<h2>Überblick</h2>
|
||||
<div class="grid-two">
|
||||
<div>
|
||||
<h3>Zielgruppe</h3>
|
||||
<ul>
|
||||
<li>3–4 HR-Mitarbeiterinnen (Schweiz)</li>
|
||||
<li>Excel: Basis + SVERWEIS</li>
|
||||
<li>Technikaffinität: 5–6/10</li>
|
||||
<li>Keine Power BI Vorkenntnisse</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Zielgruppen der Reports</h3>
|
||||
<ul>
|
||||
<li>Geschäftsleitung</li>
|
||||
<li>Verwaltungsrat</li>
|
||||
<li>Finanzbuchhaltung</li>
|
||||
<li>Abteilungsleiter</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Datenquellen</h3>
|
||||
<ul>
|
||||
<li>SAP HCM/HRM (Infotypen PA0001, PA0002, PA0008, PA2001)</li>
|
||||
<li>Rexx HR-System (Stellenplan, Pulsumfrage, MA-Zufriedenheit)</li>
|
||||
<li>Excel/CSV (Kununu-Score, Refline/Time-to-hire)</li>
|
||||
</ul>
|
||||
|
||||
<h3>KPIs (mit Periodizität)</h3>
|
||||
<ul class="kpi-list">
|
||||
<li>Headcount/FTE (monatlich)</li>
|
||||
<li>Fluktuation (monatlich)</li>
|
||||
<li>Krankenquote gesamt + ohne Langzeitkrankheiten >30 Tage (Quartal)</li>
|
||||
<li>Überstunden (Quartal)</li>
|
||||
<li>Produktivstunden (wöchentlich)</li>
|
||||
<li>Ferientage/GLZ-Saldi (jährlich)</li>
|
||||
<li>Stellenplan Soll vs Ist (monatlich, Rexx)</li>
|
||||
<li>Lohnkosten (monatlich)</li>
|
||||
<li>Time to hire (Quartal)</li>
|
||||
<li>Kununu Score (monatlich)</li>
|
||||
<li>Pulsumfrage (Quartal, Rexx)</li>
|
||||
<li>MA-Zufriedenheitsumfrage (jährlich, Rexx)</li>
|
||||
</ul>
|
||||
|
||||
<figure>
|
||||
<svg width="100%" height="260" viewBox="0 0 980 260" role="img" aria-label="Datenfluss von Quellen zu Power BI und Reports">
|
||||
<defs>
|
||||
<linearGradient id="box" x1="0" x2="1">
|
||||
<stop offset="0%" stop-color="#e0f2fe"/>
|
||||
<stop offset="100%" stop-color="#eef2ff"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
|
||||
<path d="M0,0 L9,3 L0,6 Z" fill="#64748b" />
|
||||
</marker>
|
||||
</defs>
|
||||
<rect x="20" y="30" width="240" height="60" rx="12" fill="url(#box)" stroke="#94a3b8" />
|
||||
<text x="140" y="65" text-anchor="middle" font-size="14" fill="#0f172a">SAP HCM/HRM</text>
|
||||
|
||||
<rect x="20" y="120" width="240" height="60" rx="12" fill="url(#box)" stroke="#94a3b8" />
|
||||
<text x="140" y="155" text-anchor="middle" font-size="14" fill="#0f172a">Rexx HR-System</text>
|
||||
|
||||
<rect x="20" y="210" width="240" height="60" rx="12" fill="url(#box)" stroke="#94a3b8" />
|
||||
<text x="140" y="245" text-anchor="middle" font-size="14" fill="#0f172a">Excel/CSV</text>
|
||||
|
||||
<rect x="350" y="100" width="260" height="80" rx="14" fill="#1f6feb" opacity="0.12" stroke="#1f6feb" />
|
||||
<text x="480" y="145" text-anchor="middle" font-size="16" fill="#1f6feb">Power BI Desktop</text>
|
||||
|
||||
<rect x="700" y="70" width="260" height="120" rx="14" fill="#ecfeff" stroke="#0ea5e9" />
|
||||
<text x="830" y="120" text-anchor="middle" font-size="14" fill="#0f172a">Berichte & Dashboards</text>
|
||||
<text x="830" y="145" text-anchor="middle" font-size="12" fill="#475569">GL · VR · Finanzen · Abteilungen</text>
|
||||
|
||||
<line x1="260" y1="60" x2="350" y2="120" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" />
|
||||
<line x1="260" y1="150" x2="350" y2="140" stroke="#64748b" stroke-width="2" />
|
||||
<line x1="260" y1="240" x2="350" y2="160" stroke="#64748b" stroke-width="2" />
|
||||
<line x1="610" y1="140" x2="700" y2="130" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" />
|
||||
</svg>
|
||||
<figcaption>Grafik: Datenfluss von HR-Quellen in Power BI bis zu den Zielgruppen-Reports.</figcaption>
|
||||
</figure>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Vorbereitung: Arbeitsordner & Dateien <span class="badge">Start</span></h2>
|
||||
<ol>
|
||||
<li>Lege einen Ordner <strong>HR-Power-BI</strong> an.</li>
|
||||
<li>Erstelle Unterordner: <code>01_Rohdaten</code>, <code>02_Transformiert</code>, <code>03_Berichte</code>.</li>
|
||||
<li>Speichere Exporte aus SAP/Rexx/Excel immer in <code>01_Rohdaten</code>.</li>
|
||||
<li>Benutze klare Dateinamen mit Datum, z. B. <code>SAP_PA0001_2025-01.csv</code>.</li>
|
||||
</ol>
|
||||
<div class="callout success">Ziel: Alle Teammitglieder finden Dateien sofort wieder und arbeiten mit den gleichen Daten.</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>1. Grundlagen & Datenimport <span class="badge">Modul 1</span></h2>
|
||||
<h3>1.1 Installation & erster Start</h3>
|
||||
<ol>
|
||||
<li>Gehe auf <strong>https://powerbi.microsoft.com/de-de/desktop/</strong> und lade Power BI Desktop herunter.</li>
|
||||
<li>Installiere mit Standardoptionen (Weiter → Installieren → Fertigstellen).</li>
|
||||
<li>Starte Power BI Desktop und wähle <strong>Leerer Bericht</strong>.</li>
|
||||
<li>Speichere die Datei als <code>HR-Reporting.pbix</code> in <code>03_Berichte</code>.</li>
|
||||
</ol>
|
||||
<div class="callout">Tipp: Speichere früh und oft – Power BI Desktop hat keine Auto-Speicherung.</div>
|
||||
|
||||
<h3>1.2 Oberfläche kennenlernen</h3>
|
||||
<ol>
|
||||
<li>Links: Berichtsansicht (Diagramme), Datenansicht (Tabellen), Modellansicht (Beziehungen).</li>
|
||||
<li>Rechts: Visualisierungen (Diagramm-Typ), Felder (Spalten), Filter.</li>
|
||||
<li>Oben: Menüband mit allen Funktionen.</li>
|
||||
</ol>
|
||||
<div class="callout">Merksatz: <strong>Felder</strong> sind die Daten, <strong>Visualisierungen</strong> sind die Diagramme.</div>
|
||||
|
||||
<h3>1.3 Excel importieren (Kununu, Refline)</h3>
|
||||
<ol>
|
||||
<li><strong>Start → Daten abrufen → Excel</strong>.</li>
|
||||
<li>Datei auswählen → <strong>Öffnen</strong>.</li>
|
||||
<li>Im Navigator das richtige Blatt wählen (z. B. <code>Kununu_Score</code>).</li>
|
||||
<li>Klicke <strong>Laden</strong>.</li>
|
||||
</ol>
|
||||
<div class="callout warning">Warnung: Excel-Tabellen ohne Überschrift führen zu „Spalte1/Spalte2“. Nutze in Power Query „Erste Zeile als Überschrift“.</div>
|
||||
|
||||
<h3>1.4 CSV importieren (Time-to-hire)</h3>
|
||||
<ol>
|
||||
<li><strong>Start → Daten abrufen → Text/CSV</strong>.</li>
|
||||
<li>Datei auswählen → <strong>Öffnen</strong>.</li>
|
||||
<li>Prüfe <strong>Trennzeichen</strong> (meist Semikolon).</li>
|
||||
<li>Setze <strong>Dateiursprung</strong> auf UTF-8.</li>
|
||||
</ol>
|
||||
<div class="callout">Tipp: Umlaute (ä, ö, ü) sind das beste Zeichen, ob die Kodierung stimmt.</div>
|
||||
|
||||
<h3>1.5 SAP-Exporte importieren</h3>
|
||||
<ol>
|
||||
<li>SAP-Export lokal speichern (z. B. PA0001, PA0002, PA0008, PA2001).</li>
|
||||
<li>Jeden Infotyp als eigene Tabelle laden.</li>
|
||||
<li>Tabellen sofort umbenennen: <code>Mitarbeiter_Org</code>, <code>Mitarbeiter_Personal</code>, <code>Mitarbeiter_Lohn</code>, <code>Absenzen</code>.</li>
|
||||
</ol>
|
||||
<div class="callout warning">Warnung: SAP-Daten enthalten oft führende Nullen bei Personalnummern (PERNR). Nicht löschen!</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>2. Power Query Editor <span class="badge">Modul 2</span></h2>
|
||||
<h3>2.1 Power Query öffnen</h3>
|
||||
<ol>
|
||||
<li><strong>Start → Daten transformieren</strong>.</li>
|
||||
<li>Du siehst eine Vorschau-Tabelle pro Datenquelle.</li>
|
||||
</ol>
|
||||
|
||||
<h3>2.2 Erste Zeile als Überschrift</h3>
|
||||
<ol>
|
||||
<li><strong>Transformieren → Erste Zeile als Überschriften</strong>.</li>
|
||||
<li>Kontrolliere, ob Spaltennamen sinnvoll sind.</li>
|
||||
</ol>
|
||||
|
||||
<h3>2.3 Datentypen richtig setzen</h3>
|
||||
<ol>
|
||||
<li>Datumsspalten: <strong>Datum</strong> auswählen.</li>
|
||||
<li>Zahlen: <strong>Ganze Zahl</strong> oder <strong>Dezimalzahl</strong>.</li>
|
||||
<li>Text: <strong>Text</strong>.</li>
|
||||
</ol>
|
||||
<div class="callout warning">Warnung: Schweizer Datumsformat (TT.MM.JJJJ) braucht oft „Datentyp mit Gebietsschema (Deutsch – Schweiz)“.</div>
|
||||
|
||||
<h3>2.4 Spalten entfernen / behalten</h3>
|
||||
<ol>
|
||||
<li>Unnötige Spalten markieren → <strong>Spalten entfernen</strong>.</li>
|
||||
<li>Wenn nur 6–8 Spalten relevant sind: <strong>Andere Spalten entfernen</strong>.</li>
|
||||
</ol>
|
||||
|
||||
<h3>2.5 Zeilen filtern</h3>
|
||||
<ol>
|
||||
<li>Filterpfeil in der Spalte <strong>Status</strong>.</li>
|
||||
<li>Nur aktive Mitarbeitende wählen.</li>
|
||||
<li>Zeitraum (z. B. letztes Jahr) filtern.</li>
|
||||
</ol>
|
||||
|
||||
<h3>2.6 Werte ersetzen</h3>
|
||||
<ol>
|
||||
<li><strong>Transformieren → Werte ersetzen</strong>.</li>
|
||||
<li><code>null</code> durch <code>0</code> ersetzen.</li>
|
||||
<li>Codes wie <code>A</code> in Klartext (<code>Aktiv</code>) umwandeln.</li>
|
||||
</ol>
|
||||
|
||||
<h3>2.7 Spalten teilen / zusammenführen</h3>
|
||||
<ol>
|
||||
<li>Spalte auswählen → <strong>Spalte teilen</strong> (z. B. Vorname/Nachname).</li>
|
||||
<li>Mehrere Spalten zusammenführen (z. B. Vorname + Nachname).</li>
|
||||
</ol>
|
||||
|
||||
<h3>2.8 Berechnete Spalte</h3>
|
||||
<ol>
|
||||
<li><strong>Spalte hinzufügen → Benutzerdefinierte Spalte</strong>.</li>
|
||||
<li>Beispiel: FTE = Beschäftigungsgrad / 100.</li>
|
||||
</ol>
|
||||
|
||||
<h3>2.9 Schliessen & Laden</h3>
|
||||
<ol>
|
||||
<li><strong>Start → Schliessen & Laden</strong>.</li>
|
||||
<li>„Laden in“ nutzen, wenn du nur eine Verbindung brauchst.</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>3. Datenmodell <span class="badge">Modul 3</span></h2>
|
||||
<h3>3.1 Beziehungen verstehen</h3>
|
||||
<div class="grid-three">
|
||||
<div class="flow-box">
|
||||
<strong>1:n Beziehung</strong>
|
||||
<p class="small">Eine Personalnummer in der Mitarbeitertabelle kann viele Abwesenheitszeilen haben.</p>
|
||||
</div>
|
||||
<div class="flow-box">
|
||||
<strong>1:1 Beziehung</strong>
|
||||
<p class="small">Eine Personalnummer hat genau eine Detailzeile (z. B. Stammdaten).</p>
|
||||
</div>
|
||||
<div class="flow-box">
|
||||
<strong>Filterfluss</strong>
|
||||
<p class="small">Filter sollen meistens nur in eine Richtung laufen (Einweg).</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>3.2 Beziehung erstellen</h3>
|
||||
<ol>
|
||||
<li>Modellansicht öffnen (Beziehungs-Icon links).</li>
|
||||
<li>Spalte <strong>PERNR</strong> von Tabelle A auf Tabelle B ziehen.</li>
|
||||
<li>Kardinalität prüfen (1:n) und Kreuzfilterrichtung auf Einweg setzen.</li>
|
||||
</ol>
|
||||
|
||||
<h3>3.3 Datumstabelle erstellen</h3>
|
||||
<ol>
|
||||
<li><strong>Modellierung → Neue Tabelle</strong>.</li>
|
||||
<li>DAX-Formel eingeben:</li>
|
||||
</ol>
|
||||
<pre><code>Datum = ADDCOLUMNS(
|
||||
CALENDAR(DATE(2020,1,1), TODAY()),
|
||||
"Jahr", YEAR([Date]),
|
||||
"Monat", MONTH([Date]),
|
||||
"MonatName", FORMAT([Date],"MMMM"),
|
||||
"Quartal", "Q" & QUARTER([Date]),
|
||||
"KW", WEEKNUM([Date])
|
||||
)</code></pre>
|
||||
<ol start="3">
|
||||
<li><strong>Tabellen-Tools → Als Datumstabelle markieren → Datum[Date]</strong>.</li>
|
||||
</ol>
|
||||
|
||||
<h3>3.4 PERNR als Schlüssel</h3>
|
||||
<ol>
|
||||
<li>PERNR in allen SAP-Tabellen verwenden.</li>
|
||||
<li>In Rexx/Excel dieselbe Spalte sicherstellen.</li>
|
||||
<li>Bei führenden Nullen: Datentyp Text setzen (nicht Zahl).</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>4. DAX Measures <span class="badge">Modul 4</span></h2>
|
||||
<h3>4.1 Measure vs. berechnete Spalte</h3>
|
||||
<ul>
|
||||
<li><strong>Measure:</strong> wird im Bericht berechnet, schneller und flexibler.</li>
|
||||
<li><strong>Berechnete Spalte:</strong> wird in jeder Zeile gespeichert (macht Modell grösser).</li>
|
||||
</ul>
|
||||
|
||||
<h3>4.2 Neues Measure erstellen</h3>
|
||||
<ol>
|
||||
<li><strong>Modellierung → Neues Measure</strong>.</li>
|
||||
<li>Formel eingeben und Enter drücken.</li>
|
||||
<li>Measure klar benennen (z. B. <code>Headcount</code>, <code>Fluktuation</code>).</li>
|
||||
</ol>
|
||||
|
||||
<h3>4.3 Basis-Measures für HR</h3>
|
||||
<pre><code>Headcount = COUNTROWS(Mitarbeiter)
|
||||
FTE = SUMX(Mitarbeiter, Mitarbeiter[Beschäftigungsgrad]/100)
|
||||
Krankheitstage = SUM(Abwesenheiten[Kalendertage])
|
||||
Sollarbeitstage = [Headcount] * 21
|
||||
Krankenquote = DIVIDE([Krankheitstage], [Sollarbeitstage], 0)
|
||||
Krankenquote_ohne_LZ =
|
||||
VAR KrankheitstageKurz = CALCULATE([Krankheitstage], FILTER(Abwesenheiten, Abwesenheiten[Kalendertage] <= 30))
|
||||
RETURN DIVIDE(KrankheitstageKurz, [Sollarbeitstage], 0)
|
||||
Austritte = CALCULATE(COUNTROWS(Mitarbeiter), Mitarbeiter[Austritt] <> BLANK())
|
||||
Avg_Headcount = AVERAGEX(VALUES(Datum[Monat]), [Headcount])
|
||||
Fluktuation = DIVIDE([Austritte], [Avg_Headcount], 0) * 100</code></pre>
|
||||
|
||||
<h3>4.4 Zeitintelligenz</h3>
|
||||
<pre><code>Headcount_VJ = CALCULATE([Headcount], SAMEPERIODLASTYEAR(Datum[Date]))
|
||||
Headcount_VM = CALCULATE([Headcount], PREVIOUSMONTH(Datum[Date]))
|
||||
Headcount_YTD = TOTALYTD([Headcount], Datum[Date])
|
||||
Delta_VJ = [Headcount] - [Headcount_VJ]
|
||||
Delta_VJ_Proz = DIVIDE([Delta_VJ], [Headcount_VJ], 0)</code></pre>
|
||||
|
||||
<h3>4.5 Measures formatieren</h3>
|
||||
<ol>
|
||||
<li>Measure auswählen.</li>
|
||||
<li><strong>Measure-Tools → Format</strong> (Prozent, Währung, Dezimalstellen).</li>
|
||||
</ol>
|
||||
<div class="callout">Tipp: Für Krankenquote Prozentformat mit 1 Dezimalstelle verwenden.</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>5. Visualisierungen <span class="badge">Modul 5</span></h2>
|
||||
<h3>5.1 Welche Visualisierung wofür?</h3>
|
||||
<ul>
|
||||
<li><strong>Karte/Card:</strong> Einzelne KPI-Zahl (Headcount, Fluktuation).</li>
|
||||
<li><strong>Balken:</strong> Vergleich von Abteilungen/Monaten.</li>
|
||||
<li><strong>Linie:</strong> Trendverlauf (Headcount über 12 Monate).</li>
|
||||
<li><strong>Ring:</strong> Anteil Absenzen nach Typ.</li>
|
||||
<li><strong>Tachometer:</strong> Ziel vs Ist (Stellenplan).</li>
|
||||
<li><strong>Matrix:</strong> Detailansicht mit Drill-down.</li>
|
||||
</ul>
|
||||
|
||||
<h3>5.2 Erste Visualisierung erstellen</h3>
|
||||
<ol>
|
||||
<li>Visualisierung auswählen (z. B. Karte).</li>
|
||||
<li>Feld <code>Headcount</code> in Werte ziehen.</li>
|
||||
<li>Visual rechts auf der Seite platzieren.</li>
|
||||
</ol>
|
||||
|
||||
<h3>5.3 Visualisierung formatieren</h3>
|
||||
<ol>
|
||||
<li>Visual auswählen → <strong>Format</strong> (Pinsel).</li>
|
||||
<li>Titel hinzufügen: „Headcount aktuell“.</li>
|
||||
<li>Farben gemäss Firmen-CI setzen.</li>
|
||||
</ol>
|
||||
|
||||
<h3>5.4 Filter & Slicer</h3>
|
||||
<ol>
|
||||
<li>Filterbereich öffnen.</li>
|
||||
<li>Feld <code>Abteilung</code> als Seitenfilter setzen.</li>
|
||||
<li>Slicer für Zeitraum hinzufügen.</li>
|
||||
</ol>
|
||||
|
||||
<div class="callout warning">Warnung: Zu viele Filter verwirren. Maximal 2–3 Slicer pro Seite.</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>6. Dashboard bauen <span class="badge">Modul 6</span></h2>
|
||||
<h3>6.1 Layout planen</h3>
|
||||
<ol>
|
||||
<li>Wichtigste KPIs oben links platzieren (F-Muster).</li>
|
||||
<li>Maximal 6–8 Visuals pro Seite.</li>
|
||||
<li>Genug Weissraum für bessere Lesbarkeit.</li>
|
||||
</ol>
|
||||
|
||||
<h3>6.2 Management-Übersicht (Seite 1)</h3>
|
||||
<ol>
|
||||
<li>KPI-Karten: Headcount, Krankenquote, Fluktuation, Stellenplan.</li>
|
||||
<li>Trendlinie Headcount (12 Monate).</li>
|
||||
<li>Absenzquote nach Typ als Ringdiagramm.</li>
|
||||
</ol>
|
||||
|
||||
<h3>6.3 Detailanalyse (Seite 2)</h3>
|
||||
<ol>
|
||||
<li>Matrix mit Drill-down nach Abteilung.</li>
|
||||
<li>Slicer: Zeitraum und Kostenstelle.</li>
|
||||
</ol>
|
||||
|
||||
<h3>6.4 Interaktionen</h3>
|
||||
<ol>
|
||||
<li><strong>Format → Interaktionen bearbeiten</strong>.</li>
|
||||
<li>Prüfen, ob Klick auf Balken andere Visuals filtert.</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>7. Veröffentlichen & Teilen <span class="badge">Modul 7</span></h2>
|
||||
<ol>
|
||||
<li><strong>Datei → Veröffentlichen → Arbeitsbereich wählen</strong>.</li>
|
||||
<li>Im Service Visuals anheften → Dashboard erstellen.</li>
|
||||
<li>Teilen-Link an Geschäftsleitung/Finanzen senden.</li>
|
||||
<li>Geplante Aktualisierung einrichten (Gateway für lokale Daten).</li>
|
||||
</ol>
|
||||
<div class="callout">Tipp: Teste RLS im Service immer mit „Als Rolle anzeigen“.</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>8. Troubleshooting <span class="badge">Modul 8</span></h2>
|
||||
<h3>8.1 Häufige Import-Fehler</h3>
|
||||
<ul>
|
||||
<li>Umlaute falsch → Encoding auf UTF-8 stellen.</li>
|
||||
<li>Datum als Text → Datentyp mit Gebietsschema Schweiz.</li>
|
||||
<li>Dezimaltrennzeichen falsch → Gebietsschema prüfen.</li>
|
||||
</ul>
|
||||
|
||||
<h3>8.2 DAX-Fehler</h3>
|
||||
<ul>
|
||||
<li>Zirkelbezug → berechnete Spalten vermeiden.</li>
|
||||
<li>Division durch Null → <code>DIVIDE()</code> verwenden.</li>
|
||||
<li>Filterkontext falsch → <code>CALCULATE()</code> prüfen.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>9. Anhang: Cheat Sheet & Checkliste <span class="badge">Modul 9</span></h2>
|
||||
<h3>9.1 DAX Cheat Sheet</h3>
|
||||
<pre><code>Headcount = COUNTROWS(Mitarbeiter)
|
||||
FTE = SUMX(Mitarbeiter, Mitarbeiter[Beschäftigungsgrad]/100)
|
||||
Krankenquote = DIVIDE([Krankheitstage], [Sollarbeitstage], 0)
|
||||
Fluktuation = DIVIDE([Austritte], [Avg_Headcount], 0) * 100</code></pre>
|
||||
|
||||
<h3>9.2 Checkliste: Neuer Report</h3>
|
||||
<ul class="checklist">
|
||||
<li>Datenquellen klären (SAP, Rexx, Excel/CSV).</li>
|
||||
<li>Daten importieren und bereinigen (Power Query).</li>
|
||||
<li>Beziehungen und Datumstabelle erstellen.</li>
|
||||
<li>Measures bauen und formatieren.</li>
|
||||
<li>Dashboard layouten, testen, veröffentlichen.</li>
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,320 @@
|
||||
# Power BI Schulungshandbuch für HR
|
||||
Word-Version: Nicht im Repo enthalten (Binary-Dateien werden beim PR-Erstellen nicht unterstützt).
|
||||
|
||||
Zielgruppe: 3–4 HR-Mitarbeiterinnen (Schweiz), Excel-Basis + SVERWEIS, Technikaffinität 5–6/10, keine Power BI Vorkenntnisse.
|
||||
|
||||
Datenquellen: SAP HCM/HRM (PA0001, PA0002, PA0008, PA2001), Rexx HR-System (Stellenplan, Pulsumfrage, MA-Zufriedenheit), Excel/CSV (Kununu-Score, Refline/Time-to-hire).
|
||||
|
||||
KPIs: Headcount/FTE (monatlich), Fluktuation (monatlich), Krankenquote gesamt & ohne Langzeit >30 Tage (Quartal), Überstunden (Quartal), Produktivstunden (wöchentlich), Ferientage/GLZ-Saldi (jährlich), Stellenplan Soll vs Ist (monatlich), Lohnkosten (monatlich), Time to hire (Quartal), Kununu Score (monatlich), Pulsumfrage (Quartal), MA-Zufriedenheitsumfrage (jährlich).
|
||||
|
||||
Zielgruppen der Reports: Geschäftsleitung, Verwaltungsrat, Finanzbuchhaltung, Abteilungsleiter.
|
||||
|
||||
## 1. MODUL 1: GRUNDLAGEN & DATENIMPORT
|
||||
|
||||
### 1.1 Power BI Desktop installieren und starten
|
||||
1. Schritt: Gehe auf https://powerbi.microsoft.com/de-de/desktop/ und lade Power BI Desktop herunter.
|
||||
2. Schritt: Installiere die Anwendung mit den Standardoptionen (Weiter → Installieren → Fertigstellen).
|
||||
3. Schritt: Starte Power BI Desktop über das Startmenü.
|
||||
|
||||
[Screenshot: Startfenster von Power BI Desktop mit leeren Berichtsvorlagen].
|
||||
|
||||
Tipp: Wenn der Download blockiert ist, wende Dich an die IT (Admin-Rechte erforderlich).
|
||||
|
||||
### 1.2 Oberfläche kennenlernen
|
||||
1. Schritt: Wechsle links zwischen Berichtsansicht, Datenansicht und Modellansicht.
|
||||
2. Schritt: Erkenne die Bereiche: Menüband oben, Visualisierungen rechts, Felder-Bereich rechts, Seiten-Navigation links.
|
||||
3. Schritt: Klicke auf eine leere Seite, damit Visualisierungen verfügbar werden.
|
||||
|
||||
[Screenshot: Power BI Desktop mit markierter Berichtsansicht, Visualisierungen und Felder-Bereich].
|
||||
|
||||
### 1.3 Excel-Datei importieren
|
||||
1. Schritt: Reiter → Start → Daten abrufen → Excel.
|
||||
2. Schritt: Datei auswählen → Öffnen.
|
||||
3. Schritt: Im Navigator Tabelle oder Blatt auswählen → Laden.
|
||||
|
||||
Warnung: Wenn Du im Navigator mehrere Tabellen auswählst, kann die Ladezeit steigen.
|
||||
|
||||
Häufige Probleme und Lösungen:
|
||||
1. Problem: Falsche Spaltennamen → Lösung: Erste Zeile als Header setzen (siehe Modul 2).
|
||||
2. Problem: Zahlen als Text → Lösung: Datentyp korrigieren (siehe Modul 2).
|
||||
|
||||
### 1.4 CSV importieren
|
||||
1. Schritt: Reiter → Start → Daten abrufen → Text/CSV.
|
||||
2. Schritt: Datei auswählen → Öffnen.
|
||||
3. Schritt: Im Vorschaufenster Trennzeichen und Kodierung prüfen.
|
||||
|
||||
Warnung: In der Schweiz sind Umlaute oft nur mit UTF-8 korrekt. Stelle Kodierung auf UTF-8, falls nötig.
|
||||
|
||||
Hinweis: CSV hat keine Formeln oder Formatierungen – nur Rohdaten.
|
||||
|
||||
### 1.5 SAP-Export importieren
|
||||
1. Schritt: SAP-Export (z. B. TXT/CSV/XLSX) in einen lokalen Ordner speichern.
|
||||
2. Schritt: Reiter → Start → Daten abrufen → Text/CSV oder Excel wählen.
|
||||
3. Schritt: Im Navigator prüfen, ob die erste Zeile die Spaltenüberschriften enthält.
|
||||
|
||||
Tipp: Wenn die Überschriften fehlen, nutze Power Query → Erste Zeile als Überschriften.
|
||||
|
||||
## 2. MODUL 2: POWER QUERY EDITOR
|
||||
|
||||
### 2.1 Power Query öffnen
|
||||
1. Schritt: Reiter → Start → Daten transformieren.
|
||||
|
||||
[Screenshot: Button 'Daten transformieren' im Menüband].
|
||||
|
||||
### 2.2 Erste Zeile als Header verwenden
|
||||
1. Schritt: Reiter → Transformieren → Erste Zeile als Überschriften.
|
||||
2. Schritt: Prüfe, ob die Spaltennamen korrekt sind.
|
||||
|
||||
### 2.3 Datentypen ändern
|
||||
1. Schritt: Spalte auswählen (z. B. Eintrittsdatum).
|
||||
2. Schritt: Reiter → Transformieren → Datentyp → Datum.
|
||||
3. Schritt: Bei Zahlen Datentyp → Dezimalzahl oder Ganze Zahl.
|
||||
|
||||
Warnung: Schweizer Datumsformat (TT.MM.JJJJ) wird manchmal als Text erkannt. In diesem Fall zuerst Datentyp Text, dann Datum mit Gebietsschema Schweiz (Deutsch).
|
||||
|
||||
### 2.4 Spalten entfernen/behalten
|
||||
1. Schritt: Unnötige Spalten markieren.
|
||||
2. Schritt: Reiter → Start → Spalten entfernen.
|
||||
|
||||
Tipp: Nutze "Andere Spalten entfernen", um nur relevante Spalten zu behalten.
|
||||
|
||||
### 2.5 Zeilen filtern
|
||||
1. Schritt: Filterpfeil in der Spalte Status.
|
||||
2. Schritt: Nur aktive Mitarbeitende auswählen.
|
||||
3. Schritt: Zeitraumfilter z. B. letztes Jahr.
|
||||
|
||||
### 2.6 Werte ersetzen
|
||||
1. Schritt: Reiter → Transformieren → Werte ersetzen.
|
||||
2. Schritt: null durch 0 ersetzen.
|
||||
3. Schritt: Codes (z. B. 'A') durch Klartext (z. B. 'Aktiv') ersetzen.
|
||||
|
||||
### 2.7 Spalten teilen/zusammenführen
|
||||
1. Schritt: Spalte auswählen.
|
||||
2. Schritt: Reiter → Transformieren → Spalte teilen (nach Trennzeichen).
|
||||
3. Schritt: Für Zusammenführen: Reiter → Transformieren → Spalten zusammenführen.
|
||||
|
||||
### 2.8 Berechnete Spalte hinzufügen
|
||||
1. Schritt: Reiter → Spalte hinzufügen → Benutzerdefinierte Spalte.
|
||||
2. Schritt: Formel eingeben (z. B. Beschäftigungsgrad/100).
|
||||
|
||||
### 2.9 Schliessen und Laden
|
||||
1. Schritt: Reiter → Start → Schliessen & laden.
|
||||
2. Schritt: Unterschied: "Laden" speichert in Modell, "Laden in" erlaubt gezielte Ziele (z. B. nur Verbindung).
|
||||
|
||||
## 3. MODUL 3: DATENMODELL
|
||||
|
||||
### 3.1 Zur Modellansicht wechseln
|
||||
1. Schritt: Links auf die Modellansicht (Beziehungs-Icon) klicken.
|
||||
|
||||
### 3.2 Beziehungen verstehen
|
||||
1. Schritt: 1:n = Eine Zeile in Tabelle A passt zu vielen Zeilen in Tabelle B.
|
||||
2. Schritt: 1:1 = Jede Zeile passt genau zu einer anderen Zeile.
|
||||
|
||||
Warum wichtig: Beziehungen steuern, wie Filter zwischen Tabellen fliessen.
|
||||
|
||||
### 3.3 Beziehung erstellen
|
||||
1. Schritt: Spalte in Tabelle A auf passende Spalte in Tabelle B ziehen (Drag & Drop).
|
||||
2. Schritt: Beziehung prüfen → Kardinalität und Kreuzfilterrichtung einstellen.
|
||||
|
||||
Tipp: Nutze meistens Einweg-Filterrichtung, um Mehrdeutigkeiten zu vermeiden.
|
||||
|
||||
### 3.4 Datumstabelle erstellen
|
||||
1. Schritt: Reiter → Modellierung → Neue Tabelle.
|
||||
2. Schritt: DAX-Formel einfügen:
|
||||
|
||||
```
|
||||
Datum = ADDCOLUMNS(CALENDAR(DATE(2020,1,1), TODAY()), "Jahr", YEAR([Date]), "Monat", MONTH([Date]), "MonatName", FORMAT([Date],"MMMM"), "Quartal", "Q" & QUARTER([Date]), "KW", WEEKNUM([Date]))
|
||||
```
|
||||
|
||||
3. Schritt: Reiter → Tabellen-Tools → Als Datumstabelle markieren → Datum[Date] auswählen.
|
||||
|
||||
### 3.5 PERNR als Schlüssel
|
||||
1. Schritt: Verwende die Personalnummer (PERNR) als Schlüssel zwischen allen SAP-Tabellen (PA0001, PA0002, PA0008, PA2001).
|
||||
|
||||
## 4. MODUL 4: DAX MEASURES
|
||||
|
||||
### 4.1 Was ist ein Measure vs. berechnete Spalte
|
||||
1. Schritt: Measure berechnet sich dynamisch im Berichtskontext.
|
||||
2. Schritt: Berechnete Spalte wird pro Zeile gespeichert und erhöht Modellgrösse.
|
||||
|
||||
### 4.2 Neues Measure erstellen
|
||||
1. Schritt: Reiter → Modellierung → Neues Measure.
|
||||
2. Schritt: Formel eingeben und mit Enter bestätigen.
|
||||
|
||||
### 4.3 Basis-Measures für HR
|
||||
```
|
||||
Headcount = COUNTROWS(Mitarbeiter)
|
||||
FTE = SUMX(Mitarbeiter, Mitarbeiter[Beschäftigungsgrad]/100)
|
||||
Krankheitstage = SUM(Abwesenheiten[Kalendertage])
|
||||
Sollarbeitstage = [Headcount] * 21
|
||||
Krankenquote = DIVIDE([Krankheitstage], [Sollarbeitstage], 0)
|
||||
Krankenquote_ohne_LZ =
|
||||
VAR KrankheitstageKurz = CALCULATE([Krankheitstage], FILTER(Abwesenheiten, Abwesenheiten[Kalendertage] <= 30))
|
||||
RETURN DIVIDE(KrankheitstageKurz, [Sollarbeitstage], 0)
|
||||
Austritte = CALCULATE(COUNTROWS(Mitarbeiter), Mitarbeiter[Austritt] <> BLANK())
|
||||
Avg_Headcount = AVERAGEX(VALUES(Datum[Monat]), [Headcount])
|
||||
Fluktuation = DIVIDE([Austritte], [Avg_Headcount], 0) * 100
|
||||
```
|
||||
|
||||
### 4.4 Zeitintelligenz-Measures
|
||||
```
|
||||
Headcount_VJ = CALCULATE([Headcount], SAMEPERIODLASTYEAR(Datum[Date]))
|
||||
Headcount_VM = CALCULATE([Headcount], PREVIOUSMONTH(Datum[Date]))
|
||||
Headcount_YTD = TOTALYTD([Headcount], Datum[Date])
|
||||
Delta_VJ = [Headcount] - [Headcount_VJ]
|
||||
Delta_VJ_Proz = DIVIDE([Delta_VJ], [Headcount_VJ], 0)
|
||||
```
|
||||
|
||||
### 4.5 Measures formatieren
|
||||
1. Schritt: Measure auswählen.
|
||||
2. Schritt: Reiter → Measure-Tools → Format → Prozent, Dezimalstellen, Währung einstellen.
|
||||
|
||||
## 5. MODUL 5: VISUALISIERUNGEN
|
||||
|
||||
### 5.1 Visualisierungstypen und wann verwenden
|
||||
1. Karte/Card: Einzelne KPI-Zahl (Headcount, Krankenquote).
|
||||
2. Balkendiagramm: Vergleiche (Abteilungen, Monate).
|
||||
3. Liniendiagramm: Zeitverläufe (Headcount über 12 Monate).
|
||||
4. Ringdiagramm: Anteile (Absenzen nach Typ).
|
||||
5. Tachometer: Ziel vs Ist (Stellenplan-Erfüllung).
|
||||
6. Tabelle/Matrix: Details mit Drill-down.
|
||||
|
||||
### 5.2 Erste Visualisierung erstellen
|
||||
1. Schritt: Visualisierung im Bereich Visualisierungen auswählen.
|
||||
2. Schritt: Felder per Drag & Drop in Achse/Werte ziehen.
|
||||
3. Schritt: Visualisierung auf der Seite positionieren.
|
||||
|
||||
### 5.3 Visualisierung formatieren
|
||||
1. Schritt: Visual auswählen → Reiter Visual → Format (Pinsel).
|
||||
2. Schritt: Titel, Farben, Schriftgrössen anpassen.
|
||||
|
||||
### 5.4 Filter hinzufügen
|
||||
1. Schritt: Filterbereich öffnen.
|
||||
2. Schritt: Felder in Visualfilter, Seitenfilter oder Berichtsfilter ziehen.
|
||||
|
||||
### 5.5 Slicer erstellen
|
||||
1. Schritt: Visualisierung → Datenschnitt (Slicer) wählen.
|
||||
2. Schritt: Feld (z. B. Zeitraum, Abteilung) hinzufügen.
|
||||
|
||||
### 5.6 Bedingte Formatierung
|
||||
1. Schritt: In Tabelle/Matrix auf Wertefeld klicken → Bedingte Formatierung.
|
||||
2. Schritt: Regeln definieren (z. B. Rot/Grün je nach Wert).
|
||||
|
||||
Tipp: Ampel-Logik funktioniert gut für Krankenquote und Fluktuation.
|
||||
|
||||
## 6. MODUL 6: DASHBOARD BAUEN
|
||||
|
||||
### 6.1 Dashboard-Layout planen
|
||||
1. Schritt: F-Muster beachten – Wichtigstes oben links.
|
||||
2. Schritt: Maximal 6–8 Visualisierungen pro Seite.
|
||||
|
||||
### 6.2 Seite 1: Management-Übersicht erstellen
|
||||
1. Schritt: KPI-Karten oben: Headcount, Krankenquote, Fluktuation, Stellenplan.
|
||||
2. Schritt: Trendlinie Headcount über 12 Monate.
|
||||
3. Schritt: Absenzquote nach Typ als Ringdiagramm.
|
||||
|
||||
### 6.3 Seite 2: Detailanalyse erstellen
|
||||
1. Schritt: Matrix mit Drill-down nach Abteilung.
|
||||
2. Schritt: Filter für Zeitraum und Kostenstelle (Slicer).
|
||||
|
||||
### 6.4 Interaktionen zwischen Visualisierungen
|
||||
1. Schritt: Reiter → Format → Interaktionen bearbeiten.
|
||||
2. Schritt: Prüfen, ob Klick auf Balken andere Visuals filtert oder hervorhebt.
|
||||
|
||||
### 6.5 Design-Tipps
|
||||
1. Schritt: Konsistente Farben (Firmen-CI).
|
||||
2. Schritt: Genügend Weissraum.
|
||||
3. Schritt: Beschriftungen gut lesbar.
|
||||
|
||||
## 7. MODUL 7: VERÖFFENTLICHEN & TEILEN
|
||||
|
||||
### 7.1 Power BI Service (app.powerbi.com)
|
||||
1. Schritt: Konto erstellen/anmelden.
|
||||
2. Schritt: Unterschied Desktop vs Service: Desktop = Modell/Bericht, Service = Teilen/Dashboard.
|
||||
|
||||
### 7.2 Bericht veröffentlichen
|
||||
1. Schritt: Reiter → Datei → Veröffentlichen → Arbeitsbereich wählen.
|
||||
|
||||
### 7.3 Arbeitsbereich einrichten
|
||||
1. Schritt: Im Service → Arbeitsbereich erstellen.
|
||||
2. Schritt: Zugriffsrechte für Geschäftsleitung/Finanzbuchhaltung setzen.
|
||||
|
||||
### 7.4 Dashboard erstellen (aus Bericht)
|
||||
1. Schritt: Im Service Visualisierung auswählen → Anheften.
|
||||
2. Schritt: Neues Dashboard erstellen oder bestehendes wählen.
|
||||
|
||||
### 7.5 Bericht teilen
|
||||
1. Schritt: Teilen → Link generieren.
|
||||
2. Schritt: Zugriff verwalten (Rollen/Personen).
|
||||
|
||||
### 7.6 Automatische Aktualisierung einrichten
|
||||
1. Schritt: Datensatz → Geplante Aktualisierung (täglich/wöchentlich).
|
||||
2. Schritt: Für lokale Daten Gateway einrichten (IT einbeziehen).
|
||||
|
||||
### 7.7 Row-Level Security (RLS)
|
||||
1. Schritt: Reiter → Modellierung → Rollen verwalten.
|
||||
2. Schritt: Rolle erstellen, Filter setzen: [Abteilung] = USERPRINCIPALNAME().
|
||||
|
||||
Warnung: RLS muss im Service getestet werden (Als Rolle anzeigen).
|
||||
|
||||
## 8. TROUBLESHOOTING
|
||||
|
||||
### 8.1 Häufige Fehler beim Import
|
||||
1. Problem: Encoding-Probleme (UTF-8) → Lösung: Kodierung im CSV-Import anpassen.
|
||||
2. Problem: Dezimaltrennzeichen (Punkt vs Komma) → Lösung: Datentyp mit Gebietsschema Schweiz setzen.
|
||||
3. Problem: Datum als Text → Lösung: Datentyp Datum und richtiges Gebietsschema.
|
||||
|
||||
### 8.2 Häufige DAX-Fehler
|
||||
1. Problem: Zirkelbezug → Lösung: Berechnete Spalten vermeiden, Measures nutzen.
|
||||
2. Problem: Division durch Null → Lösung: DIVIDE() verwenden.
|
||||
3. Problem: Falscher Filterkontext → Lösung: Filter mit CALCULATE prüfen.
|
||||
|
||||
### 8.3 Beziehungsprobleme
|
||||
1. Problem: Mehrdeutige Beziehungen → Lösung: Eine Beziehung aktiv, andere inaktiv setzen.
|
||||
2. Problem: Fehlende Beziehung → Lösung: Schlüsselspalten prüfen (PERNR, Datum).
|
||||
|
||||
### 8.4 Performance-Probleme
|
||||
1. Problem: Zu viele Spalten importiert → Lösung: Spalten reduzieren.
|
||||
2. Problem: Zu viele berechnete Spalten → Lösung: Measures bevorzugen.
|
||||
|
||||
## 9. ANHANG
|
||||
|
||||
### 9.1 DAX Cheat Sheet (alle HR-Formeln)
|
||||
```
|
||||
Headcount = COUNTROWS(Mitarbeiter)
|
||||
FTE = SUMX(Mitarbeiter, Mitarbeiter[Beschäftigungsgrad]/100)
|
||||
Krankheitstage = SUM(Abwesenheiten[Kalendertage])
|
||||
Sollarbeitstage = [Headcount] * 21
|
||||
Krankenquote = DIVIDE([Krankheitstage], [Sollarbeitstage], 0)
|
||||
Krankenquote_ohne_LZ = VAR KrankheitstageKurz = CALCULATE([Krankheitstage], FILTER(Abwesenheiten, Abwesenheiten[Kalendertage] <= 30))
|
||||
RETURN DIVIDE(KrankheitstageKurz, [Sollarbeitstage], 0)
|
||||
Austritte = CALCULATE(COUNTROWS(Mitarbeiter), Mitarbeiter[Austritt] <> BLANK())
|
||||
Avg_Headcount = AVERAGEX(VALUES(Datum[Monat]), [Headcount])
|
||||
Fluktuation = DIVIDE([Austritte], [Avg_Headcount], 0) * 100
|
||||
Headcount_VJ = CALCULATE([Headcount], SAMEPERIODLASTYEAR(Datum[Date]))
|
||||
Headcount_VM = CALCULATE([Headcount], PREVIOUSMONTH(Datum[Date]))
|
||||
Headcount_YTD = TOTALYTD([Headcount], Datum[Date])
|
||||
Delta_VJ = [Headcount] - [Headcount_VJ]
|
||||
Delta_VJ_Proz = DIVIDE([Delta_VJ], [Headcount_VJ], 0)
|
||||
```
|
||||
|
||||
### 9.2 Checkliste: Neuen Report erstellen
|
||||
1. Schritt: Datenquellen klären (SAP, Rexx, Excel/CSV).
|
||||
2. Schritt: Daten importieren (Modul 1).
|
||||
3. Schritt: Daten bereinigen in Power Query (Modul 2).
|
||||
4. Schritt: Beziehungen und Datumstabelle erstellen (Modul 3).
|
||||
5. Schritt: Measures erstellen (Modul 4).
|
||||
6. Schritt: Visuals bauen und formatieren (Modul 5).
|
||||
7. Schritt: Dashboard layouten (Modul 6).
|
||||
8. Schritt: Veröffentlichen und teilen (Modul 7).
|
||||
|
||||
### 9.3 Glossar
|
||||
Power Query: Datenaufbereitungstool in Power BI.
|
||||
|
||||
DAX: Formelsprache für Berechnungen in Power BI.
|
||||
|
||||
Measure: Dynamische Kennzahl, abhängig vom Filterkontext.
|
||||
|
||||
Berechnete Spalte: Feste Berechnung pro Zeile.
|
||||
|
||||
RLS: Row-Level Security für zeilenbasierte Zugriffssteuerung.
|
||||
@@ -0,0 +1,258 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de-CH">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Power BI Schulungsleitfaden für Trainer (ABAP/Webservices)</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--accent: #1f6feb;
|
||||
--accent-soft: #e0f2fe;
|
||||
--text: #0f172a;
|
||||
--muted: #475569;
|
||||
--bg: #f8fafc;
|
||||
--card: #ffffff;
|
||||
--border: #e2e8f0;
|
||||
--warning: #f97316;
|
||||
--success: #16a34a;
|
||||
--code: #0b1020;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
line-height: 1.7;
|
||||
}
|
||||
header {
|
||||
background: linear-gradient(120deg, #e0f2fe 0%, #eef2ff 100%);
|
||||
padding: 40px 24px 24px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
header h1 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 2.1rem;
|
||||
}
|
||||
header p {
|
||||
margin: 6px 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
main {
|
||||
max-width: 1050px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
}
|
||||
section {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 26px;
|
||||
margin-bottom: 22px;
|
||||
box-shadow: 0 4px 18px rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
color: #111827;
|
||||
border-bottom: 2px solid var(--border);
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
h3 {
|
||||
margin-bottom: 6px;
|
||||
color: #1e293b;
|
||||
}
|
||||
h4 {
|
||||
margin: 14px 0 6px;
|
||||
color: #1f2937;
|
||||
}
|
||||
ul, ol {
|
||||
margin: 8px 0 16px 24px;
|
||||
}
|
||||
.callout {
|
||||
border-left: 4px solid var(--accent);
|
||||
background: #eef2ff;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
margin: 12px 0;
|
||||
color: #1e293b;
|
||||
}
|
||||
.warning {
|
||||
border-left-color: var(--warning);
|
||||
background: #fff7ed;
|
||||
}
|
||||
.success {
|
||||
border-left-color: var(--success);
|
||||
background: #ecfdf3;
|
||||
}
|
||||
pre {
|
||||
background: var(--code);
|
||||
color: #e2e8f0;
|
||||
padding: 16px;
|
||||
border-radius: 10px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
code {
|
||||
font-family: "Consolas", "Courier New", monospace;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 12px;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid var(--border);
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background: #f1f5f9;
|
||||
font-weight: 600;
|
||||
}
|
||||
.small {
|
||||
font-size: 0.92rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Power BI Schulungsleitfaden für Trainer (ABAP & Daten-Webservices)</h1>
|
||||
<p>Rolle: Trainer/IT/BI (ABAP-Expertise). Fokus auf Datenbereitstellung, Webservices und Betriebsübergabe.</p>
|
||||
<p class="small">Zielgruppe der Schulung: HR-Konsumentinnen (Power BI Service, Filtern, Export).</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section>
|
||||
<h2>Zielbild</h2>
|
||||
<ul>
|
||||
<li>HR konsumiert fertige Dashboards im Power BI Service.</li>
|
||||
<li>IT/BI stellt Datenquellen bereit, pflegt Modell, DAX, Refresh, Rechte.</li>
|
||||
<li>Stabiler, dokumentierter Datenfluss (SAP → Webservice → Power BI).</li>
|
||||
</ul>
|
||||
<div class="callout success">Ergebnis: HR arbeitet schneller, IT/BI bleibt Owner von Datenqualität und Logik.</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Best Practice Rollenverteilung</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Aufgabe</th>
|
||||
<th>HR</th>
|
||||
<th>IT/BI-Team</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>KPIs definieren</td><td>✓</td><td></td></tr>
|
||||
<tr><td>Daten interpretieren</td><td>✓</td><td></td></tr>
|
||||
<tr><td>Reports anfordern</td><td>✓</td><td></td></tr>
|
||||
<tr><td>Dashboards bauen</td><td></td><td>✓</td></tr>
|
||||
<tr><td>DAX/Measures schreiben</td><td></td><td>✓</td></tr>
|
||||
<tr><td>Datenmodell pflegen</td><td></td><td>✓</td></tr>
|
||||
<tr><td>Fertige Dashboards nutzen</td><td>✓</td><td></td></tr>
|
||||
<tr><td>Filter setzen, Drill-down</td><td>✓</td><td></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Datenquellen & Webservices: Architektur</h2>
|
||||
<p>Empfohlen für SAP-HR: OData/REST-Webservices aus SAP bereitstellen, dann in Power BI Service via Gateway anbinden.</p>
|
||||
<ol>
|
||||
<li>SAP HCM/HRM (PA0001/PA0002/PA0008/PA2001) → ABAP CDS/OData.</li>
|
||||
<li>Rexx HR-System → REST/CSV-Exports oder DB-View.</li>
|
||||
<li>Excel/CSV (Kununu, Refline) → SharePoint/OneDrive Ordner.</li>
|
||||
<li>Power BI Dataset → Bericht → Dashboard.</li>
|
||||
</ol>
|
||||
<div class="callout">Ziel: Quellen entkoppeln, standardisierte Schnittstellen, minimale manuelle Exporte.</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>SAP → Webservice: Vorgehen (ABAP)</h2>
|
||||
<h3>1) CDS View mit sauberem Datenmodell</h3>
|
||||
<ul>
|
||||
<li>Erstelle CDS Views je Fachthema (z. B. Personalstamm, Absenzen, Lohn).</li>
|
||||
<li>PERNR als Schlüssel, Datum als ISO-Format (YYYY-MM-DD).</li>
|
||||
<li>Sprache und Mandant berücksichtigen.</li>
|
||||
</ul>
|
||||
|
||||
<h3>2) OData Service veröffentlichen</h3>
|
||||
<ul>
|
||||
<li>Expose CDS als OData (Fiori Elements oder Gateway).</li>
|
||||
<li>Aktiviere in /IWFND/MAINT_SERVICE.</li>
|
||||
<li>Setze Authentifizierung (SAML/OAuth/Basic nach IT-Policy).</li>
|
||||
</ul>
|
||||
|
||||
<h3>3) Performance & Paging</h3>
|
||||
<ul>
|
||||
<li>Paging aktivieren, Delta-Logik prüfen.</li>
|
||||
<li>Nur benötigte Felder liefern (Thin Views).</li>
|
||||
<li>Filter serverseitig ermöglichen (Datum, Mandant, Status).</li>
|
||||
</ul>
|
||||
|
||||
<div class="callout warning">Warnung: Zu viele Felder oder fehlende Filter führen zu langsamen Refreshs.</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Power BI Service: Datenanbindung</h2>
|
||||
<h3>Gateway & Authentifizierung</h3>
|
||||
<ol>
|
||||
<li>On-Premise Data Gateway installieren (IT/BI-Team).</li>
|
||||
<li>Datenquelle registrieren (SAP OData/REST URL).</li>
|
||||
<li>Zugangsdaten hinterlegen (Servicekonto).</li>
|
||||
</ol>
|
||||
|
||||
<h3>Dataset Konfiguration</h3>
|
||||
<ol>
|
||||
<li>Power BI Desktop: Web/OData Connector nutzen.</li>
|
||||
<li>Query-Parameter für Zeitraum/Delta definieren.</li>
|
||||
<li>Dataset veröffentlichen → Service → geplante Aktualisierung.</li>
|
||||
</ol>
|
||||
|
||||
<div class="callout success">Tipp: Einmalige Parameter (z. B. Startdatum) reduzieren Datenvolumen.</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Refresh-Strategie</h2>
|
||||
<ul>
|
||||
<li>Monatliche KPIs: Refresh täglich oder wöchentlich.</li>
|
||||
<li>Wöchentliche KPIs: Refresh täglich (Mo–Fr).</li>
|
||||
<li>Jährliche KPIs: Refresh monatlich.</li>
|
||||
</ul>
|
||||
<div class="callout">Empfehlung: Einen fixen Refresh-Zeitpunkt kommunizieren (z. B. 06:00 Uhr).</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Security & Datenschutz</h2>
|
||||
<ul>
|
||||
<li>Row-Level Security für Abteilungen (wenn nötig).</li>
|
||||
<li>HR-Reports in separatem Workspace (Zugriffsgruppen).</li>
|
||||
<li>Keine sensiblen Felder im Dataset (z. B. AHV-Nummern).</li>
|
||||
</ul>
|
||||
<div class="callout warning">Warnung: Personalnummern als Text behandeln (führende Nullen behalten).</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Trainer-Checkliste vor dem Kurs</h2>
|
||||
<ul>
|
||||
<li>Power BI Service Zugriff für HR geprüft.</li>
|
||||
<li>Mindestens 1 Testbericht bereitgestellt.</li>
|
||||
<li>Refresh läuft & Daten aktuell.</li>
|
||||
<li>Kurzanleitung für Filter/Export vorbereitet.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>FAQ aus Sicht HR (Trainer-Antworten)</h2>
|
||||
<h3>„Warum stimmen Zahlen nicht?“</h3>
|
||||
<p>Meist ist ein Filter aktiv. Bitte Filter zurücksetzen und Zeitraum prüfen.</p>
|
||||
|
||||
<h3>„Warum sehe ich keine Daten?“</h3>
|
||||
<p>Entweder fehlen Berechtigungen oder der Zeitraum ist zu eng gesetzt.</p>
|
||||
|
||||
<h3>„Kann ich Daten ändern?“</h3>
|
||||
<p>Nein. HR konsumiert, Datenpflege erfolgt in SAP/Rexx/IT.</p>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user