@@ -1482,6 +1482,10 @@ h1{font-size:21px;font-weight:600;margin-bottom:6px}
. h d r { b a c k g r o u n d : # 0 0 7 8 d 4 ; c o l o r : # f f f ; p a d d i n g : 2 2 p x 2 8 p x ; b o r d e r - r a d i u s : 1 0 p x ; m a r g i n - b o t t o m : 2 2 p x }
. h d r { b a c k g r o u n d : # 0 0 7 8 d 4 ; c o l o r : # f f f ; p a d d i n g : 2 2 p x 2 8 p x ; b o r d e r - r a d i u s : 1 0 p x ; m a r g i n - b o t t o m : 2 2 p x }
. h d r . s u b { f o n t - s i z e : 1 3 p x ; o p a c i t y : . 8 5 ; m a r g i n - t o p : 4 p x }
. h d r . s u b { f o n t - s i z e : 1 3 p x ; o p a c i t y : . 8 5 ; m a r g i n - t o p : 4 p x }
. h d r a { c o l o r : # c c e 4 f f }
. h d r a { c o l o r : # c c e 4 f f }
. s r c h { b a c k g r o u n d : # f f f ; b o r d e r - r a d i u s : 8 p x ; p a d d i n g : 1 0 p x 1 4 p x ; m a r g i n - b o t t o m : 1 4 p x ; b o x - s h a d o w : 0 1 p x 4 p x r g b a ( 0 , 0 , 0 , . 0 8 ) }
. s r c h i n p u t { w i d t h : 1 0 0 % ; p a d d i n g : 6 p x 1 0 p x ; b o r d e r : 1 p x s o l i d # c c c ; b o r d e r - r a d i u s : 4 p x ; f o n t - s i z e : 1 3 p x ; o u t l i n e : n o n e }
. s r c h i n p u t : f o c u s { b o r d e r - c o l o r : # 0 0 7 8 d 4 }
. h i d d e n { d i s p l a y : n o n e }
. c a r d s { d i s p l a y : f l e x ; g a p : 1 4 p x ; m a r g i n - b o t t o m : 2 2 p x }
. c a r d s { d i s p l a y : f l e x ; g a p : 1 4 p x ; m a r g i n - b o t t o m : 2 2 p x }
. c a r d { b a c k g r o u n d : # f f f ; b o r d e r - r a d i u s : 8 p x ; p a d d i n g : 1 6 p x 2 0 p x ; f l e x : 1 ; b o x - s h a d o w : 0 1 p x 4 p x r g b a ( 0 , 0 , 0 , . 0 8 ) ; t e x t - a l i g n : c e n t e r }
. c a r d { b a c k g r o u n d : # f f f ; b o r d e r - r a d i u s : 8 p x ; p a d d i n g : 1 6 p x 2 0 p x ; f l e x : 1 ; b o x - s h a d o w : 0 1 p x 4 p x r g b a ( 0 , 0 , 0 , . 0 8 ) ; t e x t - a l i g n : c e n t e r }
. c a r d . v { f o n t - s i z e : 2 6 p x ; f o n t - w e i g h t : 7 0 0 ; c o l o r : # 0 0 7 8 d 4 }
. c a r d . v { f o n t - s i z e : 2 6 p x ; f o n t - w e i g h t : 7 0 0 ; c o l o r : # 0 0 7 8 d 4 }
@@ -1534,9 +1538,11 @@ a:hover{text-decoration:underline}
< d i v c l a s s = " c a r d " > < d i v c l a s s = " v " > $uniqueCount < / d i v > < d i v c l a s s = " l " > U n i q u e P e r m i s s i o n S e t s < / d i v > < / d i v >
< d i v c l a s s = " c a r d " > < d i v c l a s s = " v " > $uniqueCount < / d i v > < d i v c l a s s = " l " > U n i q u e P e r m i s s i o n S e t s < / d i v > < / d i v >
< d i v c l a s s = " c a r d " > < d i v c l a s s = " v " > $userCount < / d i v > < d i v c l a s s = " l " > D i s t i n c t U s e r s / G r o u p s < / d i v > < / d i v >
< d i v c l a s s = " c a r d " > < d i v c l a s s = " v " > $userCount < / d i v > < d i v c l a s s = " l " > D i s t i n c t U s e r s / G r o u p s < / d i v > < / d i v >
< / d i v >
< / d i v >
< d i v c l a s s = " w r a p " > < t a b l e >
< d i v c l a s s = " w r a p " >
< d i v c l a s s = " s r c h " > < i n p u t t y p e = " t e x t " i d = " q " p l a c e h o l d e r = " F i l t e r r e s u l t s . . . " o n k e y u p = " f i l t e r T a b l e ( ) " > < / d i v >
< t a b l e >
< t h e a d > < t r > < t h > T y p e < / t h > < t h > N a m e < / t h > < t h > U s e r s / M e m b e r s < / t h > < t h > P e r m i s s i o n L e v e l < / t h > < t h > G r a n t e d T h r o u g h < / t h > < t h > U n i q u e P e r m i s s i o n s < / t h > < / t r > < / t h e a d >
< t h e a d > < t r > < t h > T y p e < / t h > < t h > N a m e < / t h > < t h > U s e r s / M e m b e r s < / t h > < t h > P e r m i s s i o n L e v e l < / t h > < t h > G r a n t e d T h r o u g h < / t h > < t h > U n i q u e P e r m i s s i o n s < / t h > < / t r > < / t h e a d >
< t b o d y >
< t b o d y i d = " t b o d y " >
$rows
$rows
< / t b o d y > < / t a b l e > < / d i v >
< / t b o d y > < / t a b l e > < / d i v >
< d i v c l a s s = " f o o t " > G e n e r a t e d b y S h a r e P o i n t T o o l b o x < / d i v >
< d i v c l a s s = " f o o t " > G e n e r a t e d b y S h a r e P o i n t T o o l b o x < / d i v >
@@ -1595,6 +1601,12 @@ function fallbackCopy(text) {
t r y { d o c u m e n t . e x e c C o m m a n d ( ' c o p y ' ) ; } c a t c h ( e ) { }
t r y { d o c u m e n t . e x e c C o m m a n d ( ' c o p y ' ) ; } c a t c h ( e ) { }
d o c u m e n t . b o d y . r e m o v e C h i l d ( t a ) ;
d o c u m e n t . b o d y . r e m o v e C h i l d ( t a ) ;
}
}
f u n c t i o n f i l t e r T a b l e ( ) {
v a r q = d o c u m e n t . g e t E l e m e n t B y I d ( ' q ' ) . v a l u e . t o L o w e r C a s e ( ) ;
A r r a y . f r o m ( d o c u m e n t . g e t E l e m e n t B y I d ( ' t b o d y ' ) . r o w s ) . f o r E a c h ( f u n c t i o n ( r ) {
r . c l a s s L i s t . t o g g l e ( ' h i d d e n ' , q & & ! r . i n n e r T e x t . t o L o w e r C a s e ( ) . i n c l u d e s ( q ) ) ;
} ) ;
}
< / s c r i p t >
< / s c r i p t >
< / b o d y > < / h t m l >
< / b o d y > < / h t m l >
"@
"@
@@ -1720,6 +1732,10 @@ a:hover{text-decoration:underline}
. s f - t b l t r : h o v e r t d { b a c k g r o u n d : # e a f 7 e a }
. s f - t b l t r : h o v e r t d { b a c k g r o u n d : # e a f 7 e a }
. s f - t b l a { c o l o r : # 2 e 7 d 3 2 }
. s f - t b l a { c o l o r : # 2 e 7 d 3 2 }
. f o o t { m a r g i n - t o p : 1 8 p x ; t e x t - a l i g n : c e n t e r ; f o n t - s i z e : 1 2 p x ; c o l o r : # b b b }
. f o o t { m a r g i n - t o p : 1 8 p x ; t e x t - a l i g n : c e n t e r ; f o n t - s i z e : 1 2 p x ; c o l o r : # b b b }
. s r c h { b a c k g r o u n d : # f f f ; b o r d e r - r a d i u s : 8 p x ; p a d d i n g : 1 0 p x 1 4 p x ; m a r g i n - b o t t o m : 1 4 p x ; b o x - s h a d o w : 0 1 p x 4 p x r g b a ( 0 , 0 , 0 , . 0 8 ) }
. s r c h i n p u t { w i d t h : 1 0 0 % ; p a d d i n g : 6 p x 1 0 p x ; b o r d e r : 1 p x s o l i d # c c c ; b o r d e r - r a d i u s : 4 p x ; f o n t - s i z e : 1 3 p x ; o u t l i n e : n o n e }
. s r c h i n p u t : f o c u s { b o r d e r - c o l o r : # 1 0 7 c 1 0 }
. h i d d e n { d i s p l a y : n o n e }
< / s t y l e >
< / s t y l e >
< s c r i p t >
< s c r i p t >
f u n c t i o n t o g g l e ( i ) {
f u n c t i o n t o g g l e ( i ) {
@@ -1741,12 +1757,22 @@ function toggle(i){
< d i v c l a s s = " c a r d " > < d i v c l a s s = " v " > $totalFiles < / d i v > < d i v c l a s s = " l " > T o t a l F i l e s < / d i v > < / d i v >
< d i v c l a s s = " c a r d " > < d i v c l a s s = " v " > $totalFiles < / d i v > < d i v c l a s s = " l " > T o t a l F i l e s < / d i v > < / d i v >
< d i v c l a s s = " c a r d " > < d i v c l a s s = " v " > $libCount < / d i v > < d i v c l a s s = " l " > L i b r a r i e s / S i t e s S c a n n e d < / d i v > < / d i v >
< d i v c l a s s = " c a r d " > < d i v c l a s s = " v " > $libCount < / d i v > < d i v c l a s s = " l " > L i b r a r i e s / S i t e s S c a n n e d < / d i v > < / d i v >
< / d i v >
< / d i v >
< d i v c l a s s = " w r a p " > < t a b l e >
< d i v c l a s s = " w r a p " >
< d i v c l a s s = " s r c h " > < i n p u t t y p e = " t e x t " i d = " q " p l a c e h o l d e r = " F i l t e r r e s u l t s . . . " o n k e y u p = " f i l t e r T a b l e ( ) " > < / d i v >
< t a b l e >
< t h e a d > < t r > < t h > L i b r a r y < / t h > < t h > S i t e < / t h > < t h s t y l e = " t e x t - a l i g n : r i g h t " > F i l e s < / t h > < t h s t y l e = " t e x t - a l i g n : r i g h t " > S i z e < / t h > < t h s t y l e = " t e x t - a l i g n : r i g h t " > V e r s i o n s < / t h > < t h > S h a r e o f T o t a l < / t h > < t h s t y l e = " t e x t - a l i g n : r i g h t " > L a s t M o d i f i e d < / t h > < / t r > < / t h e a d >
< t h e a d > < t r > < t h > L i b r a r y < / t h > < t h > S i t e < / t h > < t h s t y l e = " t e x t - a l i g n : r i g h t " > F i l e s < / t h > < t h s t y l e = " t e x t - a l i g n : r i g h t " > S i z e < / t h > < t h s t y l e = " t e x t - a l i g n : r i g h t " > V e r s i o n s < / t h > < t h > S h a r e o f T o t a l < / t h > < t h s t y l e = " t e x t - a l i g n : r i g h t " > L a s t M o d i f i e d < / t h > < / t r > < / t h e a d >
< t b o d y >
< t b o d y i d = " t b o d y " >
$rows
$rows
< / t b o d y > < / t a b l e > < / d i v >
< / t b o d y > < / t a b l e > < / d i v >
< d i v c l a s s = " f o o t " > G e n e r a t e d b y S h a r e P o i n t T o o l b o x < / d i v >
< d i v c l a s s = " f o o t " > G e n e r a t e d b y S h a r e P o i n t T o o l b o x < / d i v >
< s c r i p t >
f u n c t i o n f i l t e r T a b l e ( ) {
v a r q = d o c u m e n t . g e t E l e m e n t B y I d ( ' q ' ) . v a l u e . t o L o w e r C a s e ( ) ;
A r r a y . f r o m ( d o c u m e n t . g e t E l e m e n t B y I d ( ' t b o d y ' ) . r o w s ) . f o r E a c h ( f u n c t i o n ( r ) {
r . c l a s s L i s t . t o g g l e ( ' h i d d e n ' , q & & ! r . i n n e r T e x t . t o L o w e r C a s e ( ) . i n c l u d e s ( q ) ) ;
} ) ;
}
< / s c r i p t >
< / b o d y > < / h t m l >
< / b o d y > < / h t m l >
"@
"@
$html | Out-File -FilePath $OutputPath -Encoding UTF8
$html | Out-File -FilePath $OutputPath -Encoding UTF8
@@ -2828,6 +2854,32 @@ $script:LangDefault = @{
" bulk.status.creating " = " Creating... "
" bulk.status.creating " = " Creating... "
" bulk.status.ok " = " OK "
" bulk.status.ok " = " OK "
" bulk.status.error " = " Error "
" bulk.status.error " = " Error "
" tab.structure " = " Structure "
" grp.struct.csv " = " CSV Import "
" lbl.struct.desc " = " Import a CSV to create a folder tree. Each column represents a depth level. "
" btn.struct.csv " = " Load CSV... "
" grp.struct.preview " = " Preview "
" grp.struct.target " = " Target "
" lbl.struct.library " = " Target library: "
" ph.struct.library " = " Shared Documents "
" btn.struct.create " = " Create Structure "
" btn.struct.clear " = " Clear "
" struct.col.path " = " Full Path "
" struct.col.depth " = " Depth "
" tab.versions " = " Versions "
" grp.ver.keep " = " Versions to Keep "
" lbl.ver.count " = " Number of versions to keep: "
" chk.ver.date " = " Also filter by date "
" rad.ver.before " = " Keep versions before: "
" rad.ver.after " = " Keep versions after: "
" grp.ver.scope " = " Scope "
" lbl.ver.library " = " Library / Folder: "
" ph.ver.library " = " Shared Documents "
" chk.ver.recursive " = " Include subfolders (recursive) "
" chk.ver.subsites " = " Include subsites "
" chk.ver.dryrun " = " Dry run (preview only, no deletion) "
" btn.ver.run " = " Clean Versions "
" btn.ver.open " = " Open Report "
}
}
$script:Lang = $null # null = use LangDefault
$script:Lang = $null # null = use LangDefault
@@ -3580,7 +3632,137 @@ $btnBulkCreate.Size = New-Object System.Drawing.Size(200, 34)
$tabBulk . Controls . AddRange ( @ ( $grpBulkList , $btnBulkCreate ) )
$tabBulk . Controls . AddRange ( @ ( $grpBulkList , $btnBulkCreate ) )
$tabs . TabPages . AddRange ( @ ( $tabPerms , $tabStorage , $tabTemplates , $tabSearch , $tabDupes , $tabTransfer , $tabBulk ) )
# ══════════════════════════════════════════════════════════════════════════════
# Tab 8 – Structure (folder tree from CSV)
# ══════════════════════════════════════════════════════════════════════════════
$tabStruct = New-Object System . Windows . Forms . TabPage
$tabStruct . Text = T " tab.structure "
# ── CSV import + target (single row) ───────────────────────────────────────
$grpStructCsv = New-Group ( T " grp.struct.csv " ) 10 4 620 52
$lblStructDesc = New-Object System . Windows . Forms . Label
$lblStructDesc . Text = T " lbl.struct.desc "
$lblStructDesc . Location = New-Object System . Drawing . Point ( 10 , 20 )
$lblStructDesc . Size = New-Object System . Drawing . Size ( 460 , 20 )
$btnStructCsv = New-Object System . Windows . Forms . Button
$btnStructCsv . Text = T " btn.struct.csv "
$btnStructCsv . Location = New-Object System . Drawing . Point ( 490 , 18 )
$btnStructCsv . Size = New-Object System . Drawing . Size ( 118 , 26 )
$grpStructCsv . Controls . AddRange ( @ ( $lblStructDesc , $btnStructCsv ) )
# ── Preview ────────────────────────────────────────────────────────────────
$grpStructPreview = New-Group ( T " grp.struct.preview " ) 10 58 620 148
$tvStruct = New-Object System . Windows . Forms . TreeView
$tvStruct . Location = New-Object System . Drawing . Point ( 10 , 18 )
$tvStruct . Size = New-Object System . Drawing . Size ( 598 , 120 )
$tvStruct . Font = New-Object System . Drawing . Font ( " Segoe UI " , 9 )
$tvStruct . ShowLines = $true
$tvStruct . ShowPlusMinus = $true
$grpStructPreview . Controls . Add ( $tvStruct )
# ── Target + Buttons (single row) ─────────────────────────────────────────
$lblStructLib = New-Object System . Windows . Forms . Label
$lblStructLib . Text = T " lbl.struct.library "
$lblStructLib . Location = New-Object System . Drawing . Point ( 12 , 214 )
$lblStructLib . Size = New-Object System . Drawing . Size ( 110 , 20 )
$txtStructLib = New-Object System . Windows . Forms . TextBox
$txtStructLib . Location = New-Object System . Drawing . Point ( 124 , 212 )
$txtStructLib . Size = New-Object System . Drawing . Size ( 200 , 22 )
$txtStructLib . PlaceholderText = T " ph.struct.library "
$btnStructCreate = New-ActionBtn ( T " btn.struct.create " ) 340 208 ( [ System.Drawing.Color ] :: FromArgb ( 0 , 120 , 212 ) )
$btnStructCreate . Size = New-Object System . Drawing . Size ( 180 , 30 )
$btnStructClear = New-Object System . Windows . Forms . Button
$btnStructClear . Text = T " btn.struct.clear "
$btnStructClear . Location = New-Object System . Drawing . Point ( 528 , 208 )
$btnStructClear . Size = New-Object System . Drawing . Size ( 90 , 30 )
$tabStruct . Controls . AddRange ( @ ( $grpStructCsv , $grpStructPreview , $lblStructLib , $txtStructLib , $btnStructCreate , $btnStructClear ) )
# ══════════════════════════════════════════════════════════════════════════════
# Tab 9 – Version Cleanup
# ══════════════════════════════════════════════════════════════════════════════
$tabVersions = New-Object System . Windows . Forms . TabPage
$tabVersions . Text = T " tab.versions "
$tabVersions . BackColor = [ System.Drawing.Color ] :: WhiteSmoke
# ── Versions to keep ─────────────────────────────────────────────────────────
$grpVerKeep = New-Group ( T " grp.ver.keep " ) 10 4 620 110
$lblVerCount = New-Object System . Windows . Forms . Label
$lblVerCount . Text = T " lbl.ver.count "
$lblVerCount . Location = New-Object System . Drawing . Point ( 10 , 22 )
$lblVerCount . Size = New-Object System . Drawing . Size ( 220 , 20 )
$nudVerCount = New-Object System . Windows . Forms . NumericUpDown
$nudVerCount . Location = New-Object System . Drawing . Point ( 235 , 20 )
$nudVerCount . Size = New-Object System . Drawing . Size ( 70 , 22 )
$nudVerCount . Minimum = 0
$nudVerCount . Maximum = 500
$nudVerCount . Value = 5
$chkVerDate = New-Check ( T " chk.ver.date " ) 10 50 250 $false
$radVerBefore = New-Radio ( T " rad.ver.before " ) 30 74 200 $true
$radVerBefore . Enabled = $false
$radVerAfter = New-Radio ( T " rad.ver.after " ) 30 96 200 $false
$radVerAfter . Enabled = $false
$dtpVer = New-Object System . Windows . Forms . DateTimePicker
$dtpVer . Location = New-Object System . Drawing . Point ( 235 , 74 )
$dtpVer . Size = New-Object System . Drawing . Size ( 150 , 22 )
$dtpVer . Format = [ System.Windows.Forms.DateTimePickerFormat ] :: Short
$dtpVer . Enabled = $false
$chkVerDate . Add_CheckedChanged ( {
$on = $chkVerDate . Checked
$radVerBefore . Enabled = $on
$radVerAfter . Enabled = $on
$dtpVer . Enabled = $on
} )
$grpVerKeep . Controls . AddRange ( @ ( $lblVerCount , $nudVerCount , $chkVerDate , $radVerBefore , $radVerAfter , $dtpVer ) )
# ── Scope ─────────────────────────────────────────────────────────────────────
$grpVerScope = New-Group ( T " grp.ver.scope " ) 10 118 620 76
$lblVerLib = New-Object System . Windows . Forms . Label
$lblVerLib . Text = T " lbl.ver.library "
$lblVerLib . Location = New-Object System . Drawing . Point ( 10 , 22 )
$lblVerLib . Size = New-Object System . Drawing . Size ( 150 , 20 )
$txtVerLib = New-Object System . Windows . Forms . TextBox
$txtVerLib . Location = New-Object System . Drawing . Point ( 164 , 20 )
$txtVerLib . Size = New-Object System . Drawing . Size ( 230 , 22 )
$txtVerLib . PlaceholderText = T " ph.ver.library "
$chkVerRecursive = New-Check ( T " chk.ver.recursive " ) 10 48 260 $true
$chkVerSubsites = New-Check ( T " chk.ver.subsites " ) 280 48 200 $false
$grpVerScope . Controls . AddRange ( @ ( $lblVerLib , $txtVerLib , $chkVerRecursive , $chkVerSubsites ) )
# ── Options + Buttons ─────────────────────────────────────────────────────────
$chkVerDryRun = New-Check ( T " chk.ver.dryrun " ) 12 200 350 $true
$btnVerRun = New-ActionBtn ( T " btn.ver.run " ) 10 228 ( [ System.Drawing.Color ] :: FromArgb ( 180 , 60 , 20 ) )
$btnVerRun . Size = New-Object System . Drawing . Size ( 180 , 30 )
$btnVerOpen = New-Object System . Windows . Forms . Button
$btnVerOpen . Text = T " btn.ver.open "
$btnVerOpen . Location = New-Object System . Drawing . Point ( 200 , 228 )
$btnVerOpen . Size = New-Object System . Drawing . Size ( 130 , 30 )
$btnVerOpen . Enabled = $false
$tabVersions . Controls . AddRange ( @ ( $grpVerKeep , $grpVerScope , $chkVerDryRun , $btnVerRun , $btnVerOpen ) )
$tabs . TabPages . AddRange ( @ ( $tabPerms , $tabStorage , $tabTemplates , $tabSearch , $tabDupes , $tabTransfer , $tabBulk , $tabStruct , $tabVersions ) )
# ── Progress bar ───────────────────────────────────────────────────────────────
# ── Progress bar ───────────────────────────────────────────────────────────────
$progressBar = New-Object System . Windows . Forms . ProgressBar
$progressBar = New-Object System . Windows . Forms . ProgressBar
@@ -3767,6 +3949,24 @@ $_reg = {
& $_reg $script:i18nMap $btnBulkRemove " btn.bulk.remove "
& $_reg $script:i18nMap $btnBulkRemove " btn.bulk.remove "
& $_reg $script:i18nMap $btnBulkClear " btn.bulk.clear "
& $_reg $script:i18nMap $btnBulkClear " btn.bulk.clear "
& $_reg $script:i18nMap $btnBulkCreate " btn.bulk.create "
& $_reg $script:i18nMap $btnBulkCreate " btn.bulk.create "
& $_reg $script:i18nMap $lblStructDesc " lbl.struct.desc "
& $_reg $script:i18nMap $btnStructCsv " btn.struct.csv "
& $_reg $script:i18nMap $lblStructLib " lbl.struct.library "
& $_reg $script:i18nMap $btnStructCreate " btn.struct.create "
& $_reg $script:i18nMap $btnStructClear " btn.struct.clear "
# Version Cleanup tab
& $_reg $script:i18nMap $lblVerCount " lbl.ver.count "
& $_reg $script:i18nMap $chkVerDate " chk.ver.date "
& $_reg $script:i18nMap $radVerBefore " rad.ver.before "
& $_reg $script:i18nMap $radVerAfter " rad.ver.after "
& $_reg $script:i18nMap $lblVerLib " lbl.ver.library "
& $_reg $script:i18nMap $chkVerRecursive " chk.ver.recursive "
& $_reg $script:i18nMap $chkVerSubsites " chk.ver.subsites "
& $_reg $script:i18nMap $chkVerDryRun " chk.ver.dryrun "
& $_reg $script:i18nMap $btnVerRun " btn.ver.run "
& $_reg $script:i18nMap $btnVerOpen " btn.ver.open "
& $_reg $script:i18nMap $grpVerKeep " grp.ver.keep "
& $_reg $script:i18nMap $grpVerScope " grp.ver.scope "
# Tab pages
# Tab pages
& $_reg $script:i18nTabs $tabPerms " tab.perms "
& $_reg $script:i18nTabs $tabPerms " tab.perms "
@@ -3776,6 +3976,8 @@ $_reg = {
& $_reg $script:i18nTabs $tabDupes " tab.dupes "
& $_reg $script:i18nTabs $tabDupes " tab.dupes "
& $_reg $script:i18nTabs $tabTransfer " tab.transfer "
& $_reg $script:i18nTabs $tabTransfer " tab.transfer "
& $_reg $script:i18nTabs $tabBulk " tab.bulk "
& $_reg $script:i18nTabs $tabBulk " tab.bulk "
& $_reg $script:i18nTabs $tabStruct " tab.structure "
& $_reg $script:i18nTabs $tabVersions " tab.versions "
# Menu items
# Menu items
& $_reg $script:i18nMenus $menuSettings " menu.settings "
& $_reg $script:i18nMenus $menuSettings " menu.settings "
@@ -3790,10 +3992,12 @@ $script:i18nPlaceholders = [System.Collections.Generic.Dictionary[string,object]
& $_reg $script:i18nPlaceholders $txtSrchModBy " ph.modified.by "
& $_reg $script:i18nPlaceholders $txtSrchModBy " ph.modified.by "
& $_reg $script:i18nPlaceholders $txtSrchLib " ph.library "
& $_reg $script:i18nPlaceholders $txtSrchLib " ph.library "
& $_reg $script:i18nPlaceholders $txtDupLib " ph.dup.lib "
& $_reg $script:i18nPlaceholders $txtDupLib " ph.dup.lib "
& $_reg $script:i18nPlaceholders $txtStructLib " ph.struct.library "
& $_reg $script:i18nPlaceholders $txtXferSrcSite " ph.xfer.site "
& $_reg $script:i18nPlaceholders $txtXferSrcSite " ph.xfer.site "
& $_reg $script:i18nPlaceholders $txtXferSrcLib " ph.xfer.library "
& $_reg $script:i18nPlaceholders $txtXferSrcLib " ph.xfer.library "
& $_reg $script:i18nPlaceholders $txtXferDstSite " ph.xfer.site "
& $_reg $script:i18nPlaceholders $txtXferDstSite " ph.xfer.site "
& $_reg $script:i18nPlaceholders $txtXferDstLib " ph.xfer.library "
& $_reg $script:i18nPlaceholders $txtXferDstLib " ph.xfer.library "
& $_reg $script:i18nPlaceholders $txtVerLib " ph.ver.library "
#endregion
#endregion
@@ -5203,30 +5407,41 @@ $btnBulkCsv.Add_Click({
$ofd = New-Object System . Windows . Forms . OpenFileDialog
$ofd = New-Object System . Windows . Forms . OpenFileDialog
$ofd . Filter = " CSV (*.csv)|*.csv|All (*.*)|*.* "
$ofd . Filter = " CSV (*.csv)|*.csv|All (*.*)|*.* "
if ( $ofd . ShowDialog ( $form ) -ne " OK " ) { return }
if ( $ofd . ShowDialog ( $form ) -ne " OK " ) { return }
$rows = Import-Csv $ofd . FileName
# Try semicolon first (handles commas inside fields), fall back to comma
$content = Get-Content $ofd . FileName -Raw
if ( $content -match ';' ) {
$rows = Import-Csv $ofd . FileName -Delimiter ';'
} else {
$rows = Import-Csv $ofd . FileName
}
$count = 0
$count = 0
foreach ( $r in $rows ) {
foreach ( $r in $rows ) {
# Accepte d column nam es (case-insensitive via PSObject )
# Rea d columns via PSObject properti es (case-insensitive)
$name = if ( $r . Name ) { $r . Name } elseif ( $r . name ) { $r . name }
$props = @ { }
elseif ( $r . Title ) { $r . Title } elseif ( $r . titl e) { $r . title } else { " " }
foreach ( $p in $r . PSObject . Properties ) { $props [ $p . Name . ToLower ( ) ] = " $ ($p . Valu e) " . Trim ( ) }
$alias = if ( $r . Alias ) { $r . Alias } elseif ( $r . alias ) { $r . alias }
elseif ( $r . URL ) { $r . URL } elseif ( $r . url ) { $r . url } else { " " }
$type = if ( $r . Type ) { $r . Type } elseif ( $r . type ) { $r . type } else { " Team " }
$tpl = if ( $r . Template ) { $r . Template } elseif ( $r . template ) { $r . template } else { " " }
$own = if ( $r . Owners ) { $r . Owners } elseif ( $r . owners ) { $r . owners }
elseif ( $r . Owner ) { $r . Owner } elseif ( $r . owner ) { $r . owner } else { " " }
$mem = if ( $r . Members ) { $r . Members } elseif ( $r . members ) { $r . members } else { " " }
if ( -not $name -or -not $alias ) { continue }
$name = if ( $props [ 'name' ] ) { $props [ 'name' ] } elseif ( $props [ 'title' ] ) { $props [ 'title' ] } else { " " }
$alias = if ( $props [ 'alias' ] ) { $props [ 'alias' ] } elseif ( $props [ 'url' ] ) { $props [ 'url' ] } else { " " }
$type = if ( $props [ 'type' ] ) { $props [ 'type' ] } else { " Team " }
$tpl = if ( $props [ 'template' ] ) { $props [ 'template' ] } else { " " }
$own = if ( $props [ 'owners' ] ) { $props [ 'owners' ] } elseif ( $props [ 'owner' ] ) { $props [ 'owner' ] } else { " " }
$mem = if ( $props [ 'members' ] ) { $props [ 'members' ] } else { " " }
# Name is required; skip empty rows
if ( -not $name ) { continue }
# Auto-generate alias from name if not provided
if ( -not $alias ) {
$alias = $name . ToLower ( ) -replace '[^a-z0-9\-]' , '-' -replace '-+' , '-' -replace '^-|-$' , ''
}
# Normalize type
# Normalize type
if ( $type -match '^[Cc]omm' ) { $type = " Communication " } else { $type = " Team " }
if ( $type -match '^[Cc]omm' ) { $type = " Communication " } else { $type = " Team " }
Add-BulkListItem @ {
Add-BulkListItem @ {
Name = $name . Trim ( )
Name = $name
Alias = $alias . Trim ( )
Alias = $alias
Type = $type
Type = $type
Template = $tpl . Trim ( )
Template = $tpl
Owners = $own . Trim ( )
Owners = $own
Members = $mem . Trim ( )
Members = $mem
}
}
$count + +
$count + +
}
}
@@ -5314,32 +5529,83 @@ $btnBulkCreate.Add_Click({
$name = $entry . Name
$name = $entry . Name
$alias = $entry . Alias
$alias = $entry . Alias
$isTeam = $entry . Type -ne " Communication "
$isTeam = $entry . Type -ne " Communication "
$owners = @ ( $entry . Owners -split '[,;]' | ForEach-Object { $_ . Trim ( ) } | Where-Object { $_ } )
$ownerRaw = " $ ($entry . Owners)"
$members = @ ( $entry . Members -split '[,;]' | ForEach-Object { $_ . Trim ( ) } | Where-Object { $_ } )
$memberRaw = " $ ($entry . Members)"
$owners = [ string[] ] @ ( $ownerRaw -split '[,;\s]+' | Where-Object { $_ -match '\S' } )
$members = [ string[] ] @ ( $memberRaw -split '[,;\s]+' | Where-Object { $_ -match '\S' } )
$tplName = $entry . Template
$tplName = $entry . Template
BgLog " [ $idx / $total ] Creating ' $name ' (alias: $alias , type: $( $entry . Type ) )... " " White "
BgLog " [ $idx / $total ] Creating ' $name ' (alias: $alias , type: $( $entry . Type ) )... " " White "
BgLog " DEBUG owners raw=' $ownerRaw ' parsed=[ $( $owners -join '|' ) ] count= $( $owners . Count ) " " Gray "
BgLog " DEBUG members raw=' $memberRaw ' parsed=[ $( $members -join '|' ) ] count= $( $members . Count ) " " Gray "
# TeamSite requires at least one owner
if ( $isTeam -and $owners . Count -eq 0 ) {
BgLog " ERREUR : TeamSite requires at least one owner — skipping ' $name ' " " Red "
$Sync . Queue . Enqueue ( @ { Text = " ##STATUS## " ; Index = ( $idx - 1 ) ; Value = " Error: no owner " } )
$Sync . ErrCount + +
continue
}
# Update status
# Update status
$Sync . Queue . Enqueue ( @ { Text = " ##STATUS## " ; Index = ( $idx - 1 ) ; Value = " Creating... " } )
$Sync . Queue . Enqueue ( @ { Text = " ##STATUS## " ; Index = ( $idx - 1 ) ; Value = " Creating... " } )
try {
try {
# Create the site
# Create the site WITHOUT owners/members (PnP bug: odata.bind empty array)
# Current user becomes default owner; we add owners/members after creation
Connect-PnPOnline -Url $adminUrl -Interactive -ClientId $Params . ClientId
Connect-PnPOnline -Url $adminUrl -Interactive -ClientId $Params . ClientId
$newUrl = if ( $isTeam ) {
if ( $isTeam ) {
if ( $owners . Count -gt 0 ) {
BgLog " Creating TeamSite ' $alias ' (owners/members added after)... " " DarkGray "
New-PnPSite -Type TeamSite -Title $name -Alias $alias -Owners $owners -Wait
$newUrl = New-PnPSite -Type TeamSite -Title $name -Alias $alias -Wait
} else {
New-PnPSite -Type TeamSite -Title $name -Alias $alias -Wait
}
} else {
} else {
New-PnPSite -Type CommunicationSite -Title $name -Url " $base /sites/ $alias " -Wait
BgLog " Creating CommunicationSite ' $alias '... " " DarkGray "
$newUrl = New-PnPSite -Type CommunicationSite -Title $name -Url " $base /sites/ $alias " -Wait
}
}
BgLog " Site cree : $newUrl " " LightGreen "
BgLog " Site cree : $newUrl " " LightGreen "
# Connect to the new site for template + members
# Connect to the new site for owners/members/template
Connect-PnPOnline -Url $newUrl -Interactive -ClientId $Params . ClientId
Connect-PnPOnline -Url $newUrl -Interactive -ClientId $Params . ClientId
# Assign owners & members post-creation
if ( $isTeam ) {
$groupId = $null
try { $groupId = ( Get-PnPSite -Includes GroupId ) . GroupId . Guid } catch { }
if ( $groupId ) {
foreach ( $o in $owners ) {
try {
Add-PnPMicrosoft365GroupOwner -Identity $groupId -Users $o -ErrorAction Stop
BgLog " Owner added: $o " " Cyan "
} catch { BgLog " Warn owner ' $o ': $( $_ . Exception . Message ) " " DarkYellow " }
}
foreach ( $m in $members ) {
try {
Add-PnPMicrosoft365GroupMember -Identity $groupId -Users $m -ErrorAction Stop
BgLog " Member added: $m " " Cyan "
} catch { BgLog " Warn member ' $m ': $( $_ . Exception . Message ) " " DarkYellow " }
}
} else {
BgLog " Could not get M365 GroupId — owners/members not assigned " " DarkYellow "
}
} else {
# CommunicationSite — classic SharePoint groups
if ( $owners . Count -gt 0 ) {
$ownerGrp = Get-PnPGroup | Where-Object { $_ . Title -like " *Propri* " -or $_ . Title -like " *Owner* " } | Select-Object -First 1
if ( $ownerGrp ) {
foreach ( $o in $owners ) {
try { Add-PnPGroupMember -LoginName $o -Group $ownerGrp . Title -ErrorAction SilentlyContinue } catch { }
}
}
}
if ( $members . Count -gt 0 ) {
$memberGrp = Get-PnPGroup | Where-Object { $_ . Title -like " *Membre* " -or $_ . Title -like " *Member* " } | Select-Object -First 1
if ( $memberGrp ) {
foreach ( $m in $members ) {
try { Add-PnPGroupMember -LoginName $m -Group $memberGrp . Title -ErrorAction SilentlyContinue } catch { }
}
}
}
}
# Apply template if specified
# Apply template if specified
if ( $tplName -and $Params . Templates . ContainsKey ( $tplName ) ) {
if ( $tplName -and $Params . Templates . ContainsKey ( $tplName ) ) {
$tpl = $Params . Templates [ $tplName ]
$tpl = $Params . Templates [ $tplName ]
@@ -5380,30 +5646,7 @@ $btnBulkCreate.Add_Click({
}
}
}
}
# Add members
if ( $members . Count -gt 0 ) {
$memberGroup = Get-PnPGroup | Where-Object { $_ . Title -like " *Membres* " -or $_ . Title -like " *Members* " } | Select-Object -First 1
if ( $memberGroup ) {
foreach ( $m in $members ) {
try {
Add-PnPGroupMember -LoginName $m -Group $memberGroup . Title -ErrorAction SilentlyContinue
} catch { }
}
BgLog " $( $members . Count ) member(s) added. " " Cyan "
}
}
# Add owners for Communication sites (TeamSite owners set at creation)
if ( -not $isTeam -and $owners . Count -gt 0 ) {
$ownerGroup = Get-PnPGroup | Where-Object { $_ . Title -like " *Propri* " -or $_ . Title -like " *Owner* " } | Select-Object -First 1
if ( $ownerGroup ) {
foreach ( $o in $owners ) {
try {
Add-PnPGroupMember -LoginName $o -Group $ownerGroup . Title -ErrorAction SilentlyContinue
} catch { }
}
}
}
$Sync . Queue . Enqueue ( @ { Text = " ##STATUS## " ; Index = ( $idx - 1 ) ; Value = " OK " } )
$Sync . Queue . Enqueue ( @ { Text = " ##STATUS## " ; Index = ( $idx - 1 ) ; Value = " OK " } )
$Sync . CreatedSites . Add ( [ PSCustomObject ] @ {
$Sync . CreatedSites . Add ( [ PSCustomObject ] @ {
@@ -5513,6 +5756,318 @@ $btnBulkCreate.Add_Click({
#endregion
#endregion
#region ===== Structure (folder tree from CSV) =====
# Store the parsed folder paths
$script:_StructPaths = @ ( )
function Build-StructTree([string]$csvPath ) {
# Auto-detect delimiter
$raw = Get-Content $csvPath -Raw
$delim = if ( $raw -match ';' ) { ';' } else { ',' }
$rows = Import-Csv $csvPath -Delimiter $delim
$paths = [ System.Collections.Generic.List[string] ] :: new ( )
foreach ( $r in $rows ) {
$cols = @ ( $r . PSObject . Properties | ForEach-Object { " $( $_ . Value ) " . Trim ( ) } )
# Build path from non-empty columns
$parts = @ ( $cols | Where-Object { $_ -ne '' } )
if ( $parts . Count -gt 0 ) {
# Add all intermediate paths to ensure parents exist
for ( $i = 1 ; $i -le $parts . Count ; $i + + ) {
$p = ( $parts [ 0 . . ( $i - 1 ) ] -join '/' )
if ( -not $paths . Contains ( $p ) ) { $paths . Add ( $p ) }
}
}
}
$script:_StructPaths = @ ( $paths | Sort-Object )
return $script:_StructPaths
}
function Populate-StructTreeView([string[]]$paths ) {
$tvStruct . Nodes . Clear ( )
$nodeMap = @ { }
foreach ( $p in $paths ) {
$parts = $p -split '/'
$parentKey = if ( $parts . Count -gt 1 ) { ( $parts [ 0 . . ( $parts . Count - 2 ) ] -join '/' ) } else { '' }
$name = $parts [ -1 ]
$node = New-Object System . Windows . Forms . TreeNode ( $name )
$node . Tag = $p
if ( $parentKey -and $nodeMap . ContainsKey ( $parentKey ) ) {
$nodeMap [ $parentKey ] . Nodes . Add ( $node ) | Out-Null
} else {
$tvStruct . Nodes . Add ( $node ) | Out-Null
}
$nodeMap [ $p ] = $node
}
$tvStruct . ExpandAll ( )
}
$btnStructCsv . Add_Click ( {
$ofd = New-Object System . Windows . Forms . OpenFileDialog
$ofd . Filter = " CSV (*.csv)|*.csv|All (*.*)|*.* "
if ( $ofd . ShowDialog ( $form ) -ne " OK " ) { return }
try {
$paths = Build-StructTree $ofd . FileName
Populate-StructTreeView $paths
Write-Log " $( $paths . Count ) folder(s) loaded from CSV. " " LightGreen "
} catch {
Write-Log " CSV error: $( $_ . Exception . Message ) " " Red "
}
} )
$btnStructClear . Add_Click ( {
$tvStruct . Nodes . Clear ( )
$script:_StructPaths = @ ( )
Write-Log " Structure cleared. " " Gray "
} )
$btnStructCreate . Add_Click ( {
$siteUrl = $txtSiteUrl . Text . Trim ( )
$clientId = $txtClientId . Text . Trim ( )
$library = $txtStructLib . Text . Trim ( )
if ( -not $siteUrl ) { Write-Log " Site URL required. " " Red " ; return }
if ( -not $clientId ) { Write-Log " Client ID required. " " Red " ; return }
if ( -not $library ) { Write-Log " Target library required. " " Red " ; return }
if ( $script:_StructPaths . Count -eq 0 ) { Write-Log " No structure loaded. Load a CSV first. " " Red " ; return }
$btnStructCreate . Enabled = $false
$btnStructCsv . Enabled = $false
Start-ProgressAnim
Write-Log " === CREATING FOLDER STRUCTURE === " " White "
Write-Log " Target: $siteUrl / $library " " Gray "
Write-Log " Folders to create: $( $script:_StructPaths . Count ) " " Gray "
Write-Log ( " - " * 52 ) " DarkGray "
try {
Connect-PnPOnline -Url $siteUrl -Interactive -ClientId $clientId
# Get the library root
$list = Get-PnPList -Identity $library -ErrorAction Stop
$rf = Get-PnPProperty -ClientObject $list -Property RootFolder
$base = $rf . ServerRelativeUrl . TrimEnd ( '/' )
$ok = 0
$err = 0
$total = $script:_StructPaths . Count
foreach ( $p in $script:_StructPaths ) {
$folderPath = " $base / $p "
try {
# Resolve-PnPFolder creates the full path recursively
Resolve-PnPFolder -SiteRelativePath " $library / $p " -ErrorAction Stop | Out-Null
$ok + +
Write-Log " OK: $p " " LightGreen "
} catch {
$err + +
Write-Log " FAIL: $p — $( $_ . Exception . Message ) " " Red "
}
}
Write-Log " === STRUCTURE COMPLETE: $ok OK, $err error(s) === " " White "
} catch {
Write-Log " Error: $( $_ . Exception . Message ) " " Red "
} finally {
$btnStructCreate . Enabled = $true
$btnStructCsv . Enabled = $true
Stop-ProgressAnim
}
} )
# ── Version Cleanup handlers ─────────────────────────────────────────────────
$script:_VerReport = $null
$btnVerOpen . Add_Click ( {
if ( $script:_VerReport -and ( Test-Path $script:_VerReport ) ) {
Start-Process $script:_VerReport
}
} )
$btnVerRun . Add_Click ( {
# --- Gather all selected site URLs ---
$siteUrls = @ ( )
if ( $script:_CachedSites -and $script:_CachedSites . Count -gt 0 ) {
foreach ( $s in $script:_CachedSites ) {
if ( $s . Checked ) { $siteUrls + = $s . Url }
}
}
if ( $siteUrls . Count -eq 0 ) {
$single = $txtSiteUrl . Text . Trim ( )
if ( $single ) { $siteUrls = @ ( $single ) }
}
if ( $siteUrls . Count -eq 0 ) { Write-Log " Site URL required. " " Red " ; return }
$clientId = $txtClientId . Text . Trim ( )
if ( -not $clientId ) { Write-Log " Client ID required. " " Red " ; return }
$keepCount = [ int ] $nudVerCount . Value
$useDate = $chkVerDate . Checked
$dateBefore = $radVerBefore . Checked # true = keep before, false = keep after
$cutoffDate = $dtpVer . Value
$library = $txtVerLib . Text . Trim ( )
$recursive = $chkVerRecursive . Checked
$subsites = $chkVerSubsites . Checked
$dryRun = $chkVerDryRun . Checked
$btnVerRun . Enabled = $false
Start-ProgressAnim
$modeLabel = if ( $dryRun ) { " DRY RUN " } else { " LIVE " }
Write-Log " === VERSION CLEANUP ( $modeLabel ) === " " White "
Write-Log " Keep: $keepCount version(s) " " Gray "
if ( $useDate ) {
$dir = if ( $dateBefore ) { " before " } else { " after " }
Write-Log " Date filter: keep versions $dir $( $cutoffDate . ToString ( 'yyyy-MM-dd' ) ) " " Gray "
}
Write-Log ( " - " * 52 ) " DarkGray "
$report = [ System.Collections.Generic.List[object] ] :: new ( )
$totalDeleted = 0
$totalKept = 0
$totalErrors = 0
try {
foreach ( $siteUrl in $siteUrls ) {
Write-Log " Connecting to $siteUrl ... " " Gray "
Connect-PnPOnline -Url $siteUrl -Interactive -ClientId $clientId
# Collect site URLs to process (main + subsites)
$sitesToProcess = @ ( $siteUrl )
if ( $subsites ) {
try {
$subs = Get-PnPSubWeb -Recurse -ErrorAction SilentlyContinue
foreach ( $sw in $subs ) { $sitesToProcess + = $sw . Url }
} catch { }
}
foreach ( $currentSite in $sitesToProcess ) {
if ( $currentSite -ne $siteUrl ) {
try { Connect-PnPOnline -Url $currentSite -Interactive -ClientId $clientId } catch {
Write-Log " Cannot connect to subsite $currentSite — skipped " " DarkOrange "
continue
}
}
Write-Log " Processing site: $currentSite " " White "
# Get target lists
$lists = @ ( )
if ( $library ) {
try { $lists = @ ( Get-PnPList -Identity $library -ErrorAction Stop ) } catch {
Write-Log " Library ' $library ' not found — skipped " " DarkOrange "
continue
}
} else {
$lists = Get-PnPList | Where-Object { $_ . BaseTemplate -eq 101 -and $_ . Hidden -eq $false }
}
foreach ( $list in $lists ) {
Write-Log " Library: $( $list . Title ) " " Gray "
try {
$camlQuery = " <View Scope='RecursiveAll'><Query></Query><RowLimit>5000</RowLimit></View> "
if ( -not $recursive ) {
$camlQuery = " <View><Query></Query><RowLimit>5000</RowLimit></View> "
}
$items = Get-PnPListItem -List $list . Title -Query $camlQuery -ErrorAction Stop |
Where-Object { $_ . FileSystemObjectType -eq " File " }
} catch {
Write-Log " Error listing files: $( $_ . Exception . Message ) " " Red "
$totalErrors + +
continue
}
foreach ( $item in $items ) {
try {
$file = $item . FieldValues [ " FileRef " ]
$versions = Get-PnPFileVersion -Url $file -ErrorAction Stop
if ( $versions . Count -le $keepCount ) { continue }
# Sort versions oldest first (by VersionLabel numeric)
$sorted = $versions | Sort-Object { [ double ] $_ . VersionLabel }
# Determine which versions to delete
$toDelete = @ ( )
foreach ( $v in $sorted ) {
# Always keep the last $keepCount versions
$idx = [ array ] :: IndexOf ( $sorted , $v )
$remaining = $sorted . Count - $idx
if ( $remaining -le $keepCount ) { break }
# Apply date filter if enabled
if ( $useDate ) {
$vDate = [ datetime ] $v . Created
if ( $dateBefore ) {
# Keep versions before cutoff → delete versions ON or AFTER cutoff
if ( $vDate -lt $cutoffDate ) { continue }
} else {
# Keep versions after cutoff → delete versions BEFORE cutoff
if ( $vDate -ge $cutoffDate ) { continue }
}
}
$toDelete + = $v
}
if ( $toDelete . Count -eq 0 ) { continue }
$fileName = Split-Path $file -Leaf
foreach ( $v in $toDelete ) {
if ( $dryRun ) {
Write-Log " [DRY] Would delete v $( $v . VersionLabel ) of $fileName ( $( $v . Created ) ) " " DarkOrange "
} else {
try {
Remove-PnPFileVersion -Url $file -Identity $v . Id -Force -ErrorAction Stop
Write-Log " Deleted v $( $v . VersionLabel ) of $fileName " " LightGreen "
} catch {
Write-Log " Error deleting v $( $v . VersionLabel ) of $fileName — $( $_ . Exception . Message ) " " Red "
$totalErrors + +
}
}
$totalDeleted + +
}
$kept = $sorted . Count - $toDelete . Count
$totalKept + = $kept
$report . Add ( [ PSCustomObject ] @ {
Site = $currentSite
Library = $list . Title
File = $file
TotalVer = $sorted . Count
Deleted = $toDelete . Count
Kept = $kept
} )
} catch {
$totalErrors + +
}
}
}
}
}
# Export CSV report
if ( $report . Count -gt 0 ) {
$outDir = $txtOutput . Text . Trim ( )
if ( -not $outDir ) { $outDir = if ( $PSScriptRoot ) { $PSScriptRoot } else { $PWD . Path } }
if ( -not ( Test-Path $outDir ) ) { New-Item -ItemType Directory -Path $outDir | Out-Null }
$stamp = Get-Date -Format " yyyyMMdd_HHmmss "
$prefix = if ( $dryRun ) { " VersionCleanup_DryRun " } else { " VersionCleanup " }
$csvFile = Join-Path $outDir " ${prefix} _ $stamp .csv "
$report | Export-Csv -Path $csvFile -NoTypeInformation -Encoding UTF8
$script:_VerReport = $csvFile
$btnVerOpen . Enabled = $true
Write-Log " Report: $csvFile " " White "
}
Write-Log " === VERSION CLEANUP COMPLETE: $totalDeleted deleted, $totalKept kept, $totalErrors error(s) === " " White "
} catch {
Write-Log " Error: $( $_ . Exception . Message ) " " Red "
} finally {
$btnVerRun . Enabled = $true
Stop-ProgressAnim
}
} )
#endregion
# ── Initialisation : chargement des settings ───────────────────────────────
# ── Initialisation : chargement des settings ───────────────────────────────
$_settings = Load-Settings
$_settings = Load-Settings
$script:DataFolder = if ( $_settings . dataFolder -and ( Test-Path $_settings . dataFolder ) ) {
$script:DataFolder = if ( $_settings . dataFolder -and ( Test-Path $_settings . dataFolder ) ) {