Cybersecurity Analyst Job at vTech Solution, Richmond, VA

cDQ3ZFlsSC9rTHo4Z0pPSjZDQzZYcEdOREE9PQ==
  • vTech Solution
  • Richmond, VA

Job Description

Job Summary:

"use client";

import React, { useEffect, useMemo, useState } from "react";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import {
GripVertical,
Plus,
Download,
Trash2,
RefreshCw,
Edit3,
Check,
X,
Save,
} from "lucide-react";
import { Textarea } from "@/components/ui/textarea";
import { saveAs } from "file-saver";

const API_BASE = process.env.NEXT_PUBLIC_API_URL;

// Types
export type OutlineNode = {
id: string;
title: string;
description?: string;
children?: OutlineNode[];
};

type BackendSubsection = {
sub_name?: string;
sub_description?: string;
};

type BackendSection = {
section_name?: string;
section_description?: string;
subsections?: BackendSubsection[];
};

type BackendOutline = {
opportunity_name?: string;
submitted_to_name?: string;
submitted_to_email?: string;
submitted_to_phone?: string;
submitted_by_email?: string;
submitted_by_address?: string;
cover_letter_date?: string;
cover_letter_client_contact?: string;
cover_letter_subject_line?: string;
cover_letter_body?: string;
proposal_type?: string;
sections?: BackendSection[];
table_of_contents?: string[];
[k: string]: any;
};

export type OutlineBuilderProps = {
proposalId: string;
onGenerate: (outlineText: string) => void;
isLoading?: boolean;
};

// Generic helpers
const cloneSafe = (v: T): T => {
try {
return JSON.parse(JSON.stringify(v)) as T;
} catch {
return v;
}
};

const findAndRemoveNode = (tree: OutlineNode[], id: string): [OutlineNode | null, OutlineNode[]] => {
let removed: OutlineNode | null = null;
const walk = (list: OutlineNode[]): OutlineNode[] => {
let changed = false;
const out: OutlineNode[] = [];
for (let i = 0; i < list.length; i++) {
const n = list[i];
if (n.id === id) {
removed = n;
changed = true;
continue;
}
if (Array.isArray(n.children) && n.children.length) {
const newChildren = walk(n.children);
if (newChildren !== n.children) {
changed = true;
out.push({ ...n, children: newChildren });
} else {
out.push(n);
}
} else {
out.push(n);
}
}
return changed ? out : list;
};
const newTree = walk(tree);
return [removed, newTree];
};

const insertNodeAt = (tree: OutlineNode[], nodeToInsert: OutlineNode, parentId: string | null, index: number): OutlineNode[] => {
if (parentId === null) {
const safeIndex = Math.max(0, Math.min(tree.length, index));
return [...tree.slice(0, safeIndex), nodeToInsert, ...tree.slice(safeIndex)];
}

let inserted = false;
const walk = (list: OutlineNode[]): OutlineNode[] => {
let changed = false;
const out: OutlineNode[] = [];
for (let i = 0; i < list.length; i++) {
const n = list[i];
if (n.id === parentId) {
const children = Array.isArray(n.children) ? [...n.children] : [];
const safe = Math.max(0, Math.min(children.length, index));
children.splice(safe, 0, nodeToInsert);
inserted = true;
changed = true;
out.push({ ...n, children });
continue;
}
if (Array.isArray(n.children) && n.children.length) {
const nchild = walk(n.children);
if (nchild !== n.children) {
changed = true;
out.push({ ...n, children: nchild });
} else {
out.push(n);
}
} else {
out.push(n);
}
}
return changed ? out : list;
};
const newTree = walk(tree);
return inserted ? newTree : tree;
};

const findNode = (tree: OutlineNode[], id: string): OutlineNode | null => {
const walk = (list: OutlineNode[]): OutlineNode | null => {
for (let i = 0; i < list.length; i++) {
const n = list[i];
if (n.id === id) return n;
if (Array.isArray(n.children) && n.children.length) {
const f = walk(n.children);
if (f) return f;
}
}
return null;
};
return walk(tree);
};

const numberOutline = (nodes?: OutlineNode[] | null, prefix: number[] = []): { no: string; title: string }[] => {
const out: { no: string; title: string }[] = [];
const arr = Array.isArray(nodes) ? nodes : [];
arr.forEach((n, idx) => {
const no = [...prefix, idx + 1].join(".");
out.push({ no, title: n.title });
if (n.children?.length) {
const sub = numberOutline(n.children, [...prefix, idx + 1]);
out.push(...sub);
}
});
return out;
};

const outlineToText = (nodes?: OutlineNode[] | null): string =>
numberOutline(nodes).map((x) => `${x.no} ${x.title}`).join("\n");

const OutlineBuilder: React.FC = ({ proposalId, onGenerate, isLoading }) => {
const [nodes, setNodes] = useState([]);
const [outlineJson, setOutlineJson] = useState(null);
const [loadingOutline, setLoadingOutline] = useState(true);
const [saving, setSaving] = useState(false);
const [deleting, setDeleting] = useState(false);
const [draggedId, setDraggedId] = useState(null);
const [dropHint, setDropHint] = useState(null);
const [selectedTemplate, setSelectedTemplate] = useState("template.docx");

const [meta, setMeta] = useState({
opportunity_name: "",
submitted_to_name: "",
submitted_to_email: "",
submitted_to_phone: "",
cover_letter_date: "",
cover_letter_client_contact: "",
cover_letter_subject_line: "",
cover_letter_body: "",
});

const mapBackendToNodes = (outline: BackendOutline | null): OutlineNode[] => {
if (!outline) return [];
const sections = Array.isArray(outline.sections) ? outline.sections : [];
return sections.map((s, idx) => ({
id: crypto.randomUUID(),
title: s.section_name ?? `Section ${idx + 1}`,
description: s.section_description ?? "",
children: Array.isArray(s.subsections)
? s.subsections.map((sub, j) => ({
id: crypto.randomUUID(),
title: sub.sub_name ?? `Subsection ${j + 1}`,
description: sub.sub_description ?? "",
children: [],
}))
: [],
}));
};

const mapNodesToBackendSections = (list: OutlineNode[]): BackendSection[] => {
return (Array.isArray(list) ? list : []).map((n) => ({
section_name: n.title,
section_description: n.description ?? "",
subsections: Array.isArray(n.children)
? n.children.map((c) => ({
sub_name: c.title,
sub_description: c.description ?? "",
}))
: [],
}));
};

const fetchOutline = async () => {
if (!proposalId || !API_BASE) return;
try {
setLoadingOutline(true);
const res = await fetch(`${API_BASE}/api/proposals/${proposalId}/outline`, { method: "GET" });
if (!res.ok) {
const txt = await res.text().catch(() => "");
throw new Error(`GET failed: ${res.status} ${txt}`);
}
const data = await res.json();
const outline: BackendOutline = data.outline || data.outlineJson || data;
setOutlineJson(outline);
setMeta({
opportunity_name: outline.opportunity_name ?? "",
submitted_to_name: outline.submitted_to_name ?? "",
submitted_to_email: outline.submitted_to_email ?? "",
submitted_to_phone: outline.submitted_to_phone ?? "",
cover_letter_date: outline.cover_letter_date ?? "",
cover_letter_client_contact: outline.cover_letter_client_contact ?? "",
cover_letter_subject_line: outline.cover_letter_subject_line ?? "",
cover_letter_body: outline.cover_letter_body ?? "",
});
const mapped = mapBackendToNodes(outline);
setNodes(mapped);
} catch (err) {
console.error("fetchOutline error:", err);
alert("Failed to load outline.");
} finally {
setLoadingOutline(false);
}
};

useEffect(() => {
fetchOutline();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [proposalId]);

const outlineText = useMemo(() => outlineToText(nodes), [nodes]);

const removeNodeById = (id: string) => {
const [, newTree] = findAndRemoveNode(nodes as any, id);
setNodes(newTree as any);
};

const updateTitle = (id: string, text: string) => {
const node = findNode(nodes as any, id);
if (node) {
node.title = text;
setNodes([...nodes]);
}
};

const updateDescription = (id: string, desc: string) => {
const node = findNode(nodes, id);
if (node) {
const updatedNode = { ...node, description: desc };
const newNodes = nodes.map((n) => (n.id === id ? updatedNode : n));
setNodes(newNodes);
}
};

const addChild = (parentId: string) => {
const newChild: OutlineNode = {
id: crypto.randomUUID(),
title: "New Subsection",
description: "",
children: [],
};
const parentNode = findNode(nodes as any, parentId);
const newIndex = parentNode?.children?.length ?? 0;
const newTree = insertNodeAt(nodes as any, newChild as any, parentId, newIndex);
if (newTree !== nodes) setNodes(newTree as any);
};

const addRoot = () => {
setNodes([...nodes, { id: crypto.randomUUID(), title: "New Section", description: "" }]);
};

const onDragStart = (e: React.DragEvent, id: string) => {
setDraggedId(id);
e.dataTransfer.setData("text/plain", id);
};

const onDragOverGeneric = (e: React.DragEvent) => {
e.preventDefault();
};

const endDragCleanup = () => {
setDraggedId(null);
setDropHint(null);
};

useEffect(() => {
const cleanup = () => endDragCleanup();
window.addEventListener("dragend", cleanup);
return () => window.removeEventListener("dragend", cleanup);
}, []);

const findParentAndIndex = (tree: OutlineNode[], id: string): { parentId: string | null; index: number } | null => {
const walk = (arr: OutlineNode[], parentId: string | null): { parentId: string | null; index: number } | null => {
for (let i = 0; i < arr.length; i++) {
const n = arr[i];
if (n.id === id) return { parentId, index: i };
if (n.children?.length) {
const found = walk(n.children, n.id);
if (found) return found;
}
}
return null;
};
return walk(tree, null);
};

const handleDragLeaveRoot = (e: React.DragEvent) => {
const to = e.relatedTarget as HTMLElement | null;
if (!to || !to.closest(".outline-left-column")) {
setDropHint(null);
}
};

const onDropBetween = (e: React.DragEvent, parentId: string | null, index: number) => {
e.preventDefault();
let dragged = e.dataTransfer.getData("text/plain");
if (!dragged && draggedId) dragged = draggedId;
if (!dragged) return endDragCleanup();

const origin = findParentAndIndex(nodes, dragged);
const originParent = origin?.parentId ?? null;
const originIndex = origin?.index ?? -1;
let targetIndex = index;
if (originParent === parentId && originIndex < index) targetIndex = index - 1;
if (originParent === parentId && targetIndex === originIndex) return endDragCleanup();

const [removed, afterRemove] = findAndRemoveNode(nodes as any, dragged);
if (!removed) return endDragCleanup();

const newTree = insertNodeAt(afterRemove as any, removed as any, parentId, targetIndex);
if (newTree !== nodes) setNodes(newTree as any);
endDragCleanup();
};

const onDropInto = (e: React.DragEvent, parentId: string | null) => {
e.preventDefault();
let dragged = e.dataTransfer.getData("text/plain");
if (!dragged && draggedId) dragged = draggedId;
if (!dragged) return endDragCleanup();

if (parentId && isDescendant(nodes, dragged, parentId)) return endDragCleanup();

const origin = findParentAndIndex(nodes, dragged);
const originParent = origin?.parentId ?? null;
const originIndex = origin?.index ?? -1;
const [removed, afterRemove] = findAndRemoveNode(nodes as any, dragged);
if (!removed) return endDragCleanup();

const parentNode = parentId ? findNode(afterRemove as any, parentId) : null;
let newIndex = parentNode?.children?.length ?? afterRemove.length;
if (originParent === parentId && originIndex < newIndex) newIndex = Math.max(0, newIndex - 1);

const newTree = insertNodeAt(afterRemove as any, removed as any, parentId, newIndex);
if (newTree !== nodes) setNodes(newTree as any);
endDragCleanup();
};

const TitleInline: React.FC = ({ id, value }) => {
const [editing, setEditing] = useState(false);
const [val, setVal] = useState(value);

useEffect(() => setVal(value), [value]);

return (


{editing ? (

setVal(e.target.value)} />
{
updateTitle(id, val || value);
setEditing(false);
}}
>


{
setVal(value);
setEditing(false);
}}
>



) : (

{value}
setEditing(true)}>



)}

);
};

return (



{meta.opportunity_name || "Proposal Outline"}


{outlineJson?.proposal_type ? `Type: ${outlineJson.proposal_type}` : ""}




Reload


{saving ? (

Saving

) : (

Save Outline

)}

onGenerate(outlineText)} disabled={!nodes.length}>
Generate Draft

downloadDocx(selectedTemplate)} disabled={!nodes.length}>
Download DOCX


Delete Outline






{nodes.length > 0 ? (

) : (

No sections found. Add or reload.

)}



Add Top-Level Section





Cover & Metadata



Opportunity Name

setMeta({ ...meta, opportunity_name: e.target.value })
}
/>


Submitted To (Name)

setMeta({ ...meta, submitted_to_name: e.target.value })
}
/>



Email

setMeta({ ...meta, submitted_to_email: e.target.value })
}
/>


Phone

setMeta({ ...meta, submitted_to_phone: e.target.value })
}
/>


Cover Letter Date

setMeta({ ...meta, cover_letter_date: e.target.value })
}
/>


Client Contact

setMeta({
...meta,
cover_letter_client_contact: e.target.value,
})
}
/>


Subject Line

setMeta({
...meta,
cover_letter_subject_line: e.target.value,
})
}
/>


Cover Letter Body

setMeta({
...meta,
cover_letter_body: e.target.value,
})
}
/>



Template


Choose Template
setSelectedTemplate(e.target.value)}
>
template.docx



Tip: Edit outline on left. Edit cover details here. Save to Firestore or Export DOCX anytime.






);
};

export default OutlineBuilder;

Location:Richmond, Virginia, United States

Responsibilities:


- Support the development and scoping of the risk assessment plan.
- Identify, assess, and document applicable security controls.
- Collaborate with stakeholders to collect data and evidence.
- Perform detailed analysis of access control measures.
- Review security documentation and system procedures.
- Prepare draft and final risk assessment reports.
- Collect and review artifacts demonstrating compliance with applicable security controls.
- Incorporate feedback and deliver a final report.

Required Skills & Certifications:


- Bachelor's degree in Cybersecurity, Information Technology, Computer Science, or related field.
- Minimum 5+ years of experience in cybersecurity risk assessment or compliance projects.
- Strong understanding of NIST 800-53 and SEC530 Information Security Standards.
- Hands-on experience with access control evaluations and documentation of compliance evidence.
- Excellent analytical, reporting, and communication skills.

Preferred Skills & Certifications:


- Certifications such as CISSP, CISA, CISM, CRISC, or Security+.
- Experience working with state or federal agencies or similar regulatory environments.

Special Considerations:


- N/A

Scheduling:
- N/A

Job Tags

Similar Jobs

Good Samaritan

Health Information Management (HIM) Technician - FT - Long Term Care - SF Village Job at Good Samaritan

 ...earnings daily* Paid Time Off* Excellent Health, Dental and Vision Insurance* Health...  ...Enters and maintains medical professional information in the EMR.Requires knowledge and...  ...records. Supports and educates others on managing private information.Prepares and participates... 

Belcan

Tig Welding Technician Job at Belcan

Job Title: Tig Welding TechnicianLocation: Paris, ILZip Code: 61944Start Date: Right Away Job Type: Direct HirePay Rate...  ...respected enterprises. We offer excellent opportunities for contract/temporary, temp-to-hire, and direct assignments in the engineering... 

Truck with Jed Logistics

Truck Driver/CDL-A / Team Needed/Home Weekly/Dedicated Job at Truck with Jed Logistics

 ...Hiring immediately for Class A Driver! Bold Safe Trucking is a top employer of CDL Truck Drivers across the country. In addition to a great starting salary, we offer great benefits and great schedules. Come work for a great company that takes pride in its Drivers.... 

Acosta Group

Walmart Retail Specialist Job at Acosta Group

**Walmart Retail Specialist****General Information****Company:** PRE-US**Location:** NORRISTOWN, Pennsylvania, 19403**Ref #:** 129...  ...* $ 15.00**Function:** Merchandising**Employment Duration:** Part-time**Description and Requirements**As a Retail Specialist at Premium... 

TLC House & Pet Sitting Service

PT -Pet Sitting: Overnights & Vacation visits Job at TLC House & Pet Sitting Service

Job Summary:TLC House & Pet Sitting Service in Chandler, Arizona, is seeking experienced and reliable adults to join our team. We offer part-time opportunities for overnight stays, dog walking, and vacation visits, with flexible scheduling to fit your lifestyle.This...