Files
eve-signaler/frontend/src/pages/SystemView.tsx

303 lines
11 KiB
TypeScript

import { useParams, useNavigate } from "react-router-dom";
import { useEffect, useState } from "react";
import { toast } from "@/hooks/use-toast";
import { useQueryClient } from "@tanstack/react-query";
import { SystemTracker } from "@/components/SystemTracker";
import { RegionMap } from "@/components/RegionMap";
import { Header } from "@/components/Header";
import { parseSignature, parseScannedPercentage } from "@/utils/signatureParser";
import { getSystemId } from "@/utils/systemApi";
import pb from "@/lib/pocketbase";
import { SigviewRecord as Signature, SignatureRecord, SignatureNoteRulesResponse, Collections } from "@/lib/pbtypes";
export const SystemView = () => {
const { system, region } = useParams();
const navigate = useNavigate();
const queryClient = useQueryClient();
const [cleanMode, setCleanMode] = useState(false);
if (!system) {
navigate("/");
return null;
}
const saveSignature = async (signature: Signature): Promise<void> => {
console.log(signature);
try {
// Check if signature already exists
let existingRecord: SignatureRecord | null = null;
try {
existingRecord = await pb.collection('signature').getFirstListItem(`identifier='${signature.identifier}' && system='${signature.system}'`);
} catch (error) {
console.log(`Signature ${signature.identifier} not found`);
}
const newScannedPercentage = parseScannedPercentage(signature.scanned);
if (existingRecord) {
const updatedSignature: Pick<SignatureRecord, 'updated' | 'type' | 'name' | 'scanned' | 'note'> = {
updated: new Date().toISOString()
}
// Existing record has no type and our new signature has a type
if (!!!existingRecord.type && !!signature.type)
updatedSignature.type = signature.type;
// Existing record has no signame and our new signature has a signame
if (!!!existingRecord.name && !!signature.signame)
updatedSignature.name = signature.signame;
// Update existing signature only if new scan percentage is higher
const existingScannedPercentage = parseScannedPercentage(existingRecord.scanned);
if (newScannedPercentage >= existingScannedPercentage)
updatedSignature.scanned = signature.scanned;
if (!!!existingRecord.note && !!signature.note)
updatedSignature.note = signature.note;
await pb.collection('signature').update(existingRecord.id, updatedSignature);
console.log(`Updated signature ${signature.identifier}: ${existingScannedPercentage}% -> ${newScannedPercentage}%`);
} else {
// Create new signature
await pb.collection('signature').create({
system: signature.system,
identifier: signature.identifier,
name: signature.signame,
type: signature.type,
dangerous: signature.dangerous,
scanned: signature.scanned,
note: signature.note
});
console.log(`Created new signature ${signature.identifier} with ${newScannedPercentage}% scan`);
}
} catch (error) {
console.error('Failed to save signature:', error);
throw error;
}
};
const deleteSignature = async (signatureId: string): Promise<void> => {
try {
await pb.collection('signature').delete(signatureId);
// Invalidate queries to refresh the data
queryClient.invalidateQueries({ queryKey: ['signatures', system] });
toast({
title: "Signature Deleted",
description: "The signature has been successfully deleted.",
});
} catch (error) {
console.error('Failed to delete signature:', error);
toast({
title: "Delete Failed",
description: error instanceof Error ? error.message : "Failed to delete signature.",
variant: "destructive"
});
throw error;
}
};
const updateSignature = async (updatedSignature: Partial<Signature>): Promise<void> => {
try {
// Get the system ID for the current system
const systemId = await getSystemId(system);
console.log('Updating signature:', {
identifier: updatedSignature.identifier,
systemId,
system,
updatedSignature
});
// Find the signature by identifier and system
const existingRecord = await pb.collection('signature').getFirstListItem(
`identifier='${updatedSignature.identifier}' && system='${systemId}'`
);
console.log('Found existing record:', existingRecord);
// Prepare update data
const updateData: any = {
updated: new Date().toISOString()
};
if (updatedSignature.type !== undefined) {
updateData.type = updatedSignature.type;
}
if (updatedSignature.signame !== undefined) {
updateData.name = updatedSignature.signame; // Map signame to name field
}
if (updatedSignature.dangerous !== undefined) {
updateData.dangerous = updatedSignature.dangerous;
}
if (updatedSignature.scanned !== undefined) {
updateData.scanned = updatedSignature.scanned;
}
if (updatedSignature.note !== undefined) {
updateData.note = updatedSignature.note;
}
console.log('Update data:', updateData);
await pb.collection('signature').update(existingRecord.id, updateData);
// Invalidate queries to refresh the data
queryClient.invalidateQueries({ queryKey: ['signatures', system] });
toast({
title: "Signature Updated",
description: "The signature has been successfully updated.",
});
} catch (error) {
console.error('Failed to update signature:', error);
toast({
title: "Update Failed",
description: error instanceof Error ? error.message : "Failed to update signature.",
variant: "destructive"
});
throw error;
}
};
const handlePaste = async (e: ClipboardEvent) => {
const pastedText = e.clipboardData?.getData('text');
if (!pastedText?.trim()) return;
const wasCleanMode = cleanMode;
try {
const systemId = await getSystemId(system);
let rules: Array<Pick<SignatureNoteRulesResponse, 'regex' | 'note' | 'enabled'>> = [];
try {
const list = await pb.collection(Collections.SignatureNoteRules).getFullList<SignatureNoteRulesResponse>({ batch: 1000 });
rules = list.filter(r => r.enabled).map(r => ({ regex: r.regex, note: r.note, enabled: r.enabled }));
} catch { }
const lines = pastedText.trim().split('\n').filter(line => line.trim());
const parsedSignatures: Omit<Signature, 'id'>[] = [];
// Parse all signatures
for (const line of lines) {
const parsed = parseSignature(line, rules);
if (parsed) {
parsedSignatures.push({
...parsed,
system: systemId,
sysid: systemId,
identifier: parsed.identifier
});
}
}
if (parsedSignatures.length === 0) {
toast({
title: "No Valid Signatures",
description: "No valid signatures found in the pasted data. Signatures must follow the format: ABC-123.",
variant: "destructive"
});
return;
}
// If clean mode is enabled, delete signatures not in the pasted list
if (wasCleanMode) {
const existingSignatures = await pb.collection('signature').getFullList({
filter: `system='${systemId}'`
});
const pastedIdentifiers = new Set(parsedSignatures.map(sig => sig.identifier));
const signaturesToDelete = existingSignatures.filter(sig => !pastedIdentifiers.has(sig.identifier));
for (const sig of signaturesToDelete) {
await deleteSignature(sig.id);
}
if (signaturesToDelete.length > 0) {
console.log(`Deleted ${signaturesToDelete.length} signatures not in pasted data`);
}
// Turn off clean mode after use
// setCleanMode(false);
}
// Save all new/updated signatures
for (const signature of parsedSignatures) {
await saveSignature(signature as Signature);
}
// Invalidate queries to refresh the data
queryClient.invalidateQueries({ queryKey: ['signatures', system] });
toast({
title: "Success",
description: `${parsedSignatures.length} signatures processed.`
});
} catch (error) {
console.error('Failed to process signatures:', error);
toast({
title: "Error",
description: error instanceof Error ? error.message : "Failed to process signatures.",
variant: "destructive"
});
}
};
useEffect(() => {
document.addEventListener('paste', handlePaste);
return () => {
document.removeEventListener('paste', handlePaste);
};
}, [system, cleanMode]);
const breadcrumbs = [
{ label: "Universe", path: "/" },
...(region ? [{ label: region, path: `/regions/${region}` }] : []),
{ label: system }
];
return (
<div className="h-screen w-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 overflow-hidden">
<div className="h-full flex flex-col">
<Header title={`System: ${system}`} breadcrumbs={breadcrumbs} />
<div className="flex-1 overflow-hidden px-4 pb-8">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 h-full pt-6">
{/* Main content - signatures */}
<div className="lg:col-span-2 space-y-6 overflow-y-auto">
<SystemTracker
system={system}
cleanMode={cleanMode}
onCleanModeToggle={setCleanMode}
onDelete={deleteSignature}
onUpdate={updateSignature}
/>
</div>
{/* Regional overview map */}
<div className="lg:col-span-1 flex flex-col">
{region ? (
<div className="w-full aspect-square border border-purple-500/30 rounded-lg overflow-hidden flex flex-col">
<div className="flex-shrink-0 p-2 border-b border-purple-500/30 bg-black/20 backdrop-blur-sm">
<h3 className="text-white text-sm font-semibold">{region} Region</h3>
<p className="text-purple-200 text-xs">Click systems to navigate Current: {system}</p>
</div>
<div className="flex-1 min-h-0">
<RegionMap
regionName={region}
focusSystem={system}
isCompact={true}
header={false}
/>
</div>
</div>
) : (
<div className="bg-black/20 backdrop-blur-sm border border-purple-500/30 rounded-lg p-4 h-96 flex items-center justify-center">
<div className="text-center">
<div className="text-white text-sm">No region information</div>
<div className="text-purple-200 text-xs mt-1">Navigate from a region map to see regional overview</div>
</div>
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
};