feat: enhance dev management page with status updates for structure, loose files, and ghost directories
This commit is contained in:
parent
2a05dfa853
commit
fe0a5d079b
@ -32,6 +32,7 @@ export type GhostDirectory = {
|
||||
|
||||
export type FixResult = {
|
||||
userId: number;
|
||||
contractCategory?: string;
|
||||
created?: number;
|
||||
moved: number;
|
||||
skipped: number;
|
||||
@ -40,6 +41,7 @@ export type FixResult = {
|
||||
|
||||
export type FixMeta = {
|
||||
processedUsers?: number;
|
||||
createdTotal?: number;
|
||||
movedTotal?: number;
|
||||
errorCount?: number;
|
||||
};
|
||||
|
||||
@ -63,6 +63,11 @@ export default function DevManagementPage() {
|
||||
const [structureActionResults, setStructureActionResults] = React.useState<FixResult[]>([])
|
||||
const [looseActionMeta, setLooseActionMeta] = React.useState<{ processedUsers?: number; movedTotal?: number; errorCount?: number } | null>(null)
|
||||
const [looseActionResults, setLooseActionResults] = React.useState<FixResult[]>([])
|
||||
const [structureStatus, setStructureStatus] = React.useState('')
|
||||
const [looseStatus, setLooseStatus] = React.useState('')
|
||||
const [ghostStatus, setGhostStatus] = React.useState('')
|
||||
|
||||
const formatNow = () => new Date().toLocaleString()
|
||||
|
||||
const runImport = async () => {
|
||||
setError('')
|
||||
@ -93,6 +98,7 @@ export default function DevManagementPage() {
|
||||
const loadStructureIssues = async () => {
|
||||
setExoscaleError('')
|
||||
setExoscaleLoading(true)
|
||||
setStructureStatus('Refreshing list...')
|
||||
try {
|
||||
const res = await listFolderStructureIssues()
|
||||
if (!res.ok) {
|
||||
@ -101,6 +107,7 @@ export default function DevManagementPage() {
|
||||
}
|
||||
setStructureUsers(res.data || [])
|
||||
setStructureMeta(res.meta || null)
|
||||
setStructureStatus(`Last refresh: ${formatNow()}`)
|
||||
} finally {
|
||||
setExoscaleLoading(false)
|
||||
}
|
||||
@ -109,6 +116,7 @@ export default function DevManagementPage() {
|
||||
const loadLooseFiles = async () => {
|
||||
setExoscaleError('')
|
||||
setExoscaleLoading(true)
|
||||
setLooseStatus('Refreshing list...')
|
||||
try {
|
||||
const res = await listLooseFiles()
|
||||
if (!res.ok) {
|
||||
@ -117,6 +125,7 @@ export default function DevManagementPage() {
|
||||
}
|
||||
setLooseUsers(res.data || [])
|
||||
setLooseMeta(res.meta || null)
|
||||
setLooseStatus(`Last refresh: ${formatNow()}`)
|
||||
} finally {
|
||||
setExoscaleLoading(false)
|
||||
}
|
||||
@ -125,6 +134,7 @@ export default function DevManagementPage() {
|
||||
const loadGhostDirectories = async () => {
|
||||
setExoscaleError('')
|
||||
setExoscaleLoading(true)
|
||||
setGhostStatus('Refreshing list...')
|
||||
try {
|
||||
const res = await listGhostDirectories()
|
||||
if (!res.ok) {
|
||||
@ -133,6 +143,7 @@ export default function DevManagementPage() {
|
||||
}
|
||||
setGhostDirs(res.data || [])
|
||||
setGhostMeta(res.meta || null)
|
||||
setGhostStatus(`Last refresh: ${formatNow()}`)
|
||||
} finally {
|
||||
setExoscaleLoading(false)
|
||||
}
|
||||
@ -140,24 +151,40 @@ export default function DevManagementPage() {
|
||||
|
||||
const runCreateStructure = async (userId?: number) => {
|
||||
setExoscaleError('')
|
||||
if (userId) setFixingUserId(userId)
|
||||
else setFixingAll(true)
|
||||
if (userId) {
|
||||
setFixingUserId(userId)
|
||||
setStructureStatus(`Creating folders for #${userId}...`)
|
||||
} else {
|
||||
setFixingAll(true)
|
||||
setStructureStatus('Creating folders for each user...')
|
||||
setStructureActionResults([])
|
||||
setStructureActionMeta({ processedUsers: 0, createdTotal: 0, errorCount: 0 })
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await createFolderStructure(userId)
|
||||
const targets = userId ? [{ userId }] : structureUsers.map(u => ({ userId: u.userId }))
|
||||
for (const target of targets) {
|
||||
const res = await createFolderStructure(target.userId)
|
||||
if (!res.ok) {
|
||||
setExoscaleError(res.message || 'Failed to create folder structure.')
|
||||
return
|
||||
break
|
||||
}
|
||||
setStructureActionMeta(res.meta || null)
|
||||
setStructureActionResults(res.data || [])
|
||||
const batch = res.data || []
|
||||
setStructureActionResults(prev => [...prev, ...batch])
|
||||
setStructureActionMeta(prev => ({
|
||||
processedUsers: (prev?.processedUsers || 0) + (res.meta?.processedUsers || 0),
|
||||
createdTotal: (prev?.createdTotal || 0) + (res.meta?.createdTotal || 0),
|
||||
errorCount: (prev?.errorCount || 0) + (res.meta?.errorCount || 0)
|
||||
}))
|
||||
setFixResults(prev => {
|
||||
const next = { ...prev }
|
||||
for (const item of res.data || []) {
|
||||
for (const item of batch) {
|
||||
next[item.userId] = item
|
||||
}
|
||||
return next
|
||||
})
|
||||
setStructureStatus(`Last create: ${formatNow()} (processed #${target.userId})`)
|
||||
}
|
||||
await loadStructureIssues()
|
||||
} finally {
|
||||
setFixingUserId(null)
|
||||
@ -167,24 +194,40 @@ export default function DevManagementPage() {
|
||||
|
||||
const runMoveLooseFiles = async (userId?: number) => {
|
||||
setExoscaleError('')
|
||||
if (userId) setFixingUserId(userId)
|
||||
else setFixingAll(true)
|
||||
if (userId) {
|
||||
setFixingUserId(userId)
|
||||
setLooseStatus(`Moving loose files for #${userId}...`)
|
||||
} else {
|
||||
setFixingAll(true)
|
||||
setLooseStatus('Moving loose files for each user...')
|
||||
setLooseActionResults([])
|
||||
setLooseActionMeta({ processedUsers: 0, movedTotal: 0, errorCount: 0 })
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await moveLooseFilesToContract(userId)
|
||||
const targets = userId ? [{ userId }] : looseUsers.map(u => ({ userId: u.userId }))
|
||||
for (const target of targets) {
|
||||
const res = await moveLooseFilesToContract(target.userId)
|
||||
if (!res.ok) {
|
||||
setExoscaleError(res.message || 'Failed to move loose files.')
|
||||
return
|
||||
break
|
||||
}
|
||||
setLooseActionMeta(res.meta || null)
|
||||
setLooseActionResults(res.data || [])
|
||||
const batch = res.data || []
|
||||
setLooseActionResults(prev => [...prev, ...batch])
|
||||
setLooseActionMeta(prev => ({
|
||||
processedUsers: (prev?.processedUsers || 0) + (res.meta?.processedUsers || 0),
|
||||
movedTotal: (prev?.movedTotal || 0) + (res.meta?.movedTotal || 0),
|
||||
errorCount: (prev?.errorCount || 0) + (res.meta?.errorCount || 0)
|
||||
}))
|
||||
setFixResults(prev => {
|
||||
const next = { ...prev }
|
||||
for (const item of res.data || []) {
|
||||
for (const item of batch) {
|
||||
next[item.userId] = item
|
||||
}
|
||||
return next
|
||||
})
|
||||
setLooseStatus(`Last move: ${formatNow()} (processed #${target.userId})`)
|
||||
}
|
||||
await loadLooseFiles()
|
||||
} finally {
|
||||
setFixingUserId(null)
|
||||
@ -192,6 +235,12 @@ export default function DevManagementPage() {
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (activeTab === 'structure') loadStructureIssues()
|
||||
if (activeTab === 'loose') loadLooseFiles()
|
||||
if (activeTab === 'ghost') loadGhostDirectories()
|
||||
}, [activeTab])
|
||||
|
||||
const onImportFile = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
@ -374,6 +423,9 @@ export default function DevManagementPage() {
|
||||
<p className="text-sm text-gray-600">
|
||||
Ensures both contract and gdpr folders exist for each user.
|
||||
</p>
|
||||
{structureStatus && (
|
||||
<div className="text-xs text-slate-500">{structureStatus}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
|
||||
<button
|
||||
@ -461,6 +513,9 @@ export default function DevManagementPage() {
|
||||
{structureActionResults.map(item => (
|
||||
<div key={item.userId} className="flex flex-wrap gap-2">
|
||||
<span className="font-semibold text-blue-900">#{item.userId}</span>
|
||||
{item.contractCategory && (
|
||||
<span className="text-gray-500">{item.contractCategory}</span>
|
||||
)}
|
||||
<span>Created: {item.created ?? 0}</span>
|
||||
{item.errors && item.errors.length > 0 && (
|
||||
<span className="text-red-700">Errors: {item.errors.length}</span>
|
||||
@ -482,6 +537,9 @@ export default function DevManagementPage() {
|
||||
<p className="text-sm text-gray-600">
|
||||
Shows files directly under the user folder that are not in contract or gdpr.
|
||||
</p>
|
||||
{looseStatus && (
|
||||
<div className="text-xs text-slate-500">{looseStatus}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
|
||||
<button
|
||||
@ -575,6 +633,9 @@ export default function DevManagementPage() {
|
||||
{looseActionResults.map(item => (
|
||||
<div key={item.userId} className="flex flex-wrap gap-2">
|
||||
<span className="font-semibold text-blue-900">#{item.userId}</span>
|
||||
{item.contractCategory && (
|
||||
<span className="text-gray-500">{item.contractCategory}</span>
|
||||
)}
|
||||
<span>Moved: {item.moved}</span>
|
||||
<span>Skipped: {item.skipped}</span>
|
||||
{item.errors && item.errors.length > 0 && (
|
||||
@ -597,6 +658,9 @@ export default function DevManagementPage() {
|
||||
<p className="text-sm text-gray-600">
|
||||
Exoscale directories that do not have a matching user in the database.
|
||||
</p>
|
||||
{ghostStatus && (
|
||||
<div className="text-xs text-slate-500">{ghostStatus}</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={loadGhostDirectories}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user