Public Access
- Retour visuel au survol d'un créneau libre (zone surlignée + heure choisie qui suit la souris ; bulle fixe plage libre + durée disponible, jamais par-dessus un rendez-vous). - Rendez-vous en erreur (début = fin) affiché en rouge et visible en échelle exacte ; avertissement + blocage à la création par-dessus. Interventions courtes visibles. - Zone du nom du technicien pleine hauteur ; barre des heures collée sous la banderole. - Fiabilité : navigation rapide entre dates, technicien retiré, deadlock de synchro du dossier, jour lu depuis le dossier d'emblée, doublon d'absence, réservations multiples le même jour, barre de progression et message de reconnexion. - Synchro : bouton « Tester la communication », panneau réorganisé, script du poste 2026.1.8 (verrou d'index, icône persistante).
226 lines
8.4 KiB
PowerShell
226 lines
8.4 KiB
PowerShell
# Planification - icone barre des taches (best-effort, optionnel) // 2026.1.4 - QRO
|
|
# Developpe par Quentin Rouiller (QRO) - DGNSI, Canton de Vaud.
|
|
# Licence MIT - voir le fichier LICENSE.
|
|
#
|
|
# Indique que le helper de sync est installe. Clic / menu -> fenetre du journal
|
|
# (aspect application : journal colore par niveau, auto-rafraichi, bouton
|
|
# "Tout afficher"). Lance par tray.vbs (fenetre masquee), au login.
|
|
|
|
$ErrorActionPreference = 'SilentlyContinue'
|
|
|
|
# Instance unique.
|
|
$mutex = New-Object System.Threading.Mutex($false, 'Local\PlanificationTraySingleton')
|
|
if (-not $mutex.WaitOne(0)) { exit }
|
|
|
|
Add-Type -AssemblyName System.Windows.Forms
|
|
Add-Type -AssemblyName System.Drawing
|
|
|
|
# tray.ps1 vit dans <install>\bin -> racine = parent ; assets et log a la racine.
|
|
$root = if ($PSScriptRoot) { Split-Path -Parent $PSScriptRoot } else { Join-Path $env:LOCALAPPDATA 'Planification' }
|
|
$logFile = Join-Path $root 'activity.log'
|
|
$iconPng = Join-Path (Join-Path $root 'assets') 'icon.png'
|
|
|
|
function Get-TrayIcon {
|
|
try {
|
|
if (Test-Path -LiteralPath $iconPng) {
|
|
$bmp = New-Object System.Drawing.Bitmap($iconPng)
|
|
return [System.Drawing.Icon]::FromHandle($bmp.GetHicon())
|
|
}
|
|
} catch {}
|
|
return [System.Drawing.SystemIcons]::Information
|
|
}
|
|
|
|
$script:logForm = $null
|
|
$script:logRtb = $null
|
|
$script:logChk = $null
|
|
$script:logSig = ''
|
|
|
|
$colInfo = [System.Drawing.Color]::FromArgb(40, 70, 120)
|
|
$colDebug = [System.Drawing.Color]::FromArgb(150, 150, 150)
|
|
$colErr = [System.Drawing.Color]::FromArgb(200, 40, 40)
|
|
$colTs = [System.Drawing.Color]::FromArgb(120, 120, 120)
|
|
|
|
function Update-LogText {
|
|
if (-not $script:logRtb -or $script:logRtb.IsDisposed) { return }
|
|
$lines = @()
|
|
if (Test-Path -LiteralPath $logFile) {
|
|
try {
|
|
$all = Get-Content -LiteralPath $logFile -Encoding UTF8 -ErrorAction SilentlyContinue
|
|
if ($all) {
|
|
if ($script:logChk -and -not $script:logChk.Checked) {
|
|
$all = $all | Where-Object { $_ -notmatch '\[DEBUG\]' }
|
|
}
|
|
$lines = @($all | Select-Object -Last 500)
|
|
}
|
|
} catch {}
|
|
}
|
|
$sig = ($lines -join "`n")
|
|
if ($sig -eq $script:logSig) { return }
|
|
$script:logSig = $sig
|
|
|
|
$rtb = $script:logRtb
|
|
$rtb.SuspendLayout()
|
|
$rtb.Clear()
|
|
if ($lines.Count -eq 0) {
|
|
$rtb.SelectionColor = $colDebug
|
|
$rtb.AppendText("(aucune activite de synchronisation pour le moment)")
|
|
} else {
|
|
foreach ($line in $lines) {
|
|
$col = $colInfo
|
|
if ($line -match '\[ERREUR\]') { $col = $colErr }
|
|
elseif ($line -match '\[DEBUG\]') { $col = $colDebug }
|
|
if ($line -match '^(\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2})(.*)$') {
|
|
$rtb.SelectionStart = $rtb.TextLength; $rtb.SelectionColor = $colTs
|
|
$rtb.AppendText($Matches[1])
|
|
$rtb.SelectionStart = $rtb.TextLength; $rtb.SelectionColor = $col
|
|
$rtb.AppendText($Matches[2] + "`n")
|
|
} else {
|
|
$rtb.SelectionStart = $rtb.TextLength; $rtb.SelectionColor = $col
|
|
$rtb.AppendText($line + "`n")
|
|
}
|
|
}
|
|
}
|
|
$rtb.ResumeLayout()
|
|
$rtb.SelectionStart = $rtb.TextLength; $rtb.ScrollToCaret()
|
|
}
|
|
|
|
function Show-LogWindow {
|
|
if ($script:logForm -and -not $script:logForm.IsDisposed) {
|
|
$script:logForm.WindowState = 'Normal'; $script:logForm.Activate(); return
|
|
}
|
|
$form = New-Object System.Windows.Forms.Form
|
|
$form.Text = 'Planification - journal de synchronisation'
|
|
$form.Size = New-Object System.Drawing.Size(760, 540)
|
|
$form.StartPosition = 'CenterScreen'
|
|
$form.BackColor = [System.Drawing.Color]::FromArgb(245, 246, 248)
|
|
try { $form.Icon = Get-TrayIcon } catch {}
|
|
|
|
# 1) zone de texte (Fill) ajoutee en premier
|
|
$rtb = New-Object System.Windows.Forms.RichTextBox
|
|
$rtb.Dock = 'Fill'
|
|
$rtb.ReadOnly = $true
|
|
$rtb.BorderStyle = 'None'
|
|
$rtb.BackColor = [System.Drawing.Color]::White
|
|
$rtb.Font = New-Object System.Drawing.Font('Consolas', 9.5)
|
|
$rtb.DetectUrls = $false
|
|
$form.Controls.Add($rtb)
|
|
|
|
# 2) barre d'outils claire avec le BOUTON bascule "Tout afficher" bien visible
|
|
$bar = New-Object System.Windows.Forms.Panel
|
|
$bar.Dock = 'Top'; $bar.Height = 40
|
|
$bar.BackColor = [System.Drawing.Color]::FromArgb(238, 240, 243)
|
|
$chk = New-Object System.Windows.Forms.CheckBox
|
|
$chk.Appearance = 'Button'
|
|
$chk.Text = 'Tout afficher (mode detaille)'
|
|
$chk.TextAlign = 'MiddleCenter'
|
|
$chk.AutoSize = $false
|
|
$chk.Size = New-Object System.Drawing.Size(200, 28)
|
|
$chk.Location = New-Object System.Drawing.Point(10, 6)
|
|
$chk.FlatStyle = 'System'
|
|
$chk.add_CheckedChanged({ $script:logSig = '~'; Update-LogText })
|
|
$bar.Controls.Add($chk)
|
|
$hint = New-Object System.Windows.Forms.Label
|
|
$hint.Text = 'Par defaut : l''essentiel. Activez pour voir tout le detail technique.'
|
|
$hint.ForeColor = [System.Drawing.Color]::FromArgb(90, 90, 90)
|
|
$hint.AutoSize = $true
|
|
$hint.Location = New-Object System.Drawing.Point(220, 12)
|
|
$bar.Controls.Add($hint)
|
|
$form.Controls.Add($bar)
|
|
|
|
# 3) bandeau titre (bleu) ajoute en dernier -> tout en haut
|
|
$hdrBar = New-Object System.Windows.Forms.Panel
|
|
$hdrBar.Dock = 'Top'; $hdrBar.Height = 32
|
|
$hdrBar.BackColor = [System.Drawing.Color]::FromArgb(40, 70, 120)
|
|
$hdr = New-Object System.Windows.Forms.Label
|
|
$hdr.Text = 'Activite de synchronisation - Planification'
|
|
$hdr.ForeColor = [System.Drawing.Color]::White
|
|
$hdr.Font = New-Object System.Drawing.Font('Segoe UI', 10, [System.Drawing.FontStyle]::Bold)
|
|
$hdr.AutoSize = $true
|
|
$hdr.Location = New-Object System.Drawing.Point(10, 7)
|
|
$hdrBar.Controls.Add($hdr)
|
|
$form.Controls.Add($hdrBar)
|
|
|
|
$script:logForm = $form
|
|
$script:logRtb = $rtb
|
|
$script:logChk = $chk
|
|
$script:logSig = ''
|
|
Update-LogText
|
|
|
|
$timer = New-Object System.Windows.Forms.Timer
|
|
$timer.Interval = 2000
|
|
$timer.add_Tick({ Update-LogText })
|
|
$timer.Start()
|
|
$form.add_FormClosed({ $timer.Stop(); $timer.Dispose() })
|
|
|
|
$form.Show(); $form.Activate()
|
|
}
|
|
|
|
$ni = New-Object System.Windows.Forms.NotifyIcon
|
|
$ni.Icon = Get-TrayIcon
|
|
$ni.Text = 'Planification - synchronisation active'
|
|
$ni.Visible = $true
|
|
|
|
# v2026.1.8 - "toujours present si actif" : le NotifyIcon disparait quand
|
|
# explorer.exe redemarre (Windows rediffuse alors le message "TaskbarCreated").
|
|
# Sans le traiter, l'icone ne revient JAMAIS tant que le tray tourne. On ecoute
|
|
# ce message via une petite fenetre native et on re-affiche l'icone.
|
|
try {
|
|
Add-Type -ReferencedAssemblies System.Windows.Forms -TypeDefinition @'
|
|
using System;
|
|
using System.Windows.Forms;
|
|
using System.Runtime.InteropServices;
|
|
public class PlanifTrayWatcher : NativeWindow {
|
|
[DllImport("user32.dll", CharSet = CharSet.Auto)]
|
|
static extern uint RegisterWindowMessage(string lpString);
|
|
public static readonly uint TaskbarCreated = RegisterWindowMessage("TaskbarCreated");
|
|
private NotifyIcon _icon;
|
|
public PlanifTrayWatcher(NotifyIcon icon) {
|
|
_icon = icon;
|
|
this.CreateHandle(new CreateParams());
|
|
}
|
|
protected override void WndProc(ref Message m) {
|
|
if (m.Msg == TaskbarCreated && _icon != null) {
|
|
try { _icon.Visible = false; _icon.Visible = true; } catch {}
|
|
}
|
|
base.WndProc(ref m);
|
|
}
|
|
}
|
|
'@ -ErrorAction Stop
|
|
$script:trayWatcher = New-Object PlanifTrayWatcher($ni)
|
|
} catch {}
|
|
|
|
$menu = New-Object System.Windows.Forms.ContextMenuStrip
|
|
$miLog = $menu.Items.Add('Voir le journal de synchronisation')
|
|
$miLog.add_Click({ Show-LogWindow })
|
|
$miQuit = $menu.Items.Add('Quitter')
|
|
$miQuit.add_Click({ $ni.Visible = $false; $ni.Dispose(); [System.Windows.Forms.Application]::Exit() })
|
|
$ni.ContextMenuStrip = $menu
|
|
$ni.add_MouseClick({ param($s, $e) if ($e.Button -eq [System.Windows.Forms.MouseButtons]::Left) { Show-LogWindow } })
|
|
|
|
# v2026.1.7 - auto-extinction apres 2 h SANS activite de synchronisation. On se
|
|
# base sur la date de derniere ecriture du journal (le host y ecrit a chaque
|
|
# operation). Le navigateur relancera le host a la prochaine activite, qui
|
|
# relancera ce tray. -> "de base eteint, s'active a l'usage, se ferme au repos".
|
|
$IDLE_LIMIT_MIN = 120
|
|
$idleTimer = New-Object System.Windows.Forms.Timer
|
|
$idleTimer.Interval = 300000 # verifie toutes les 5 min
|
|
$idleTimer.add_Tick({
|
|
try {
|
|
$age = $null
|
|
if (Test-Path -LiteralPath $logFile) {
|
|
$age = ((Get-Date) - (Get-Item -LiteralPath $logFile).LastWriteTime).TotalMinutes
|
|
}
|
|
# Pas de journal, ou journal inactif depuis >= 2 h -> on s'eteint.
|
|
if (($null -eq $age) -or ($age -ge $IDLE_LIMIT_MIN)) {
|
|
$ni.Visible = $false; $ni.Dispose()
|
|
[System.Windows.Forms.Application]::Exit()
|
|
}
|
|
} catch {}
|
|
})
|
|
$idleTimer.Start()
|
|
|
|
[System.Windows.Forms.Application]::EnableVisualStyles()
|
|
[System.Windows.Forms.Application]::Run()
|
|
$idleTimer.Stop()
|
|
$ni.Dispose()
|