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