Fix certification update

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-04-22 12:13:04 -07:00
parent be0f73c1df
commit e8ba5fc6f9

View File

@@ -402,57 +402,6 @@ function ComplianceReportsTable({
); );
} }
function TagList({
tags,
onAdd,
onRemove,
}: {
tags: readonly string[];
onAdd: (tag: string) => void;
onRemove: (tag: string) => void;
}) {
const [newTag, setNewTag] = useState("");
const handleAddTag = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && newTag.trim()) {
e.preventDefault();
onAdd(newTag.trim());
setNewTag("");
}
};
return (
<div className="space-y-2">
<div className="flex flex-wrap gap-2">
{tags.map((tag) => (
<div
key={tag}
className="flex items-center gap-1 rounded-full bg-[rgba(5,77,5,0.03)] px-3 py-1 text-sm font-medium"
>
<span className="text-[#141E12]">{tag}</span>
<button
onClick={() => onRemove(tag)}
className="text-primary hover:text-primary/80"
>
<X className="h-3 w-3" />
</button>
</div>
))}
</div>
<div className="bg-[rgba(5,77,5,0.03)] rounded-lg p-1">
<Input
type="text"
value={newTag}
onChange={(e) => setNewTag(e.target.value)}
onKeyDown={handleAddTag}
placeholder="Type and press Enter to add a certification"
className="border-0 bg-transparent p-1 shadow-none focus-visible:ring-0"
/>
</div>
</div>
);
}
function CertificationsView({ function CertificationsView({
certifications, certifications,
onAdd, onAdd,
@@ -466,7 +415,7 @@ function CertificationsView({
const securityStandards = ["SOC 2", "ISO 27001", "HITRUST", "NIST"]; const securityStandards = ["SOC 2", "ISO 27001", "HITRUST", "NIST"];
// Regulatory and legal certifications // Regulatory and legal certifications
const regulatoryLegal = ["HIPAA", "FERPA", "FISMA", "PIPEDA"]; const regulatoryLegal = ["HIPAA", "FERPA", "FISMA", "PIPEDA", "GDPR", "CCPA"];
// Industry-specific certifications // Industry-specific certifications
const industrySpecific = ["FinTech", "MPAA", "GSMA"]; const industrySpecific = ["FinTech", "MPAA", "GSMA"];
@@ -474,17 +423,34 @@ function CertificationsView({
// International & government certifications // International & government certifications
const internationalGov = ["FedRAMP", "ENS High", "IRAP", "CJIS"]; const internationalGov = ["FedRAMP", "ENS High", "IRAP", "CJIS"];
// Check if certification is in vendor's list // State for new certification input
const isCertificationActive = (cert: string) => { const [showAddModal, setShowAddModal] = useState(false);
return certifications.includes(cert); const [newCertification, setNewCertification] = useState("");
// Handle adding a new custom certification
const handleAddCertification = () => {
if (newCertification.trim()) {
onAdd(newCertification.trim());
setNewCertification("");
setShowAddModal(false);
}
}; };
// Get certifications by category
const securityCerts = certifications.filter(cert => securityStandards.includes(cert));
const regulatoryCerts = certifications.filter(cert => regulatoryLegal.includes(cert));
const industryCerts = certifications.filter(cert => industrySpecific.includes(cert));
const internationalCerts = certifications.filter(cert => internationalGov.includes(cert));
const customCerts = certifications.filter(cert =>
![...securityStandards, ...regulatoryLegal, ...industrySpecific, ...internationalGov].includes(cert)
);
return ( return (
<div className="rounded-xl border border-[#ECEFEC] bg-white overflow-hidden"> <div className="rounded-xl border border-[#ECEFEC] bg-white overflow-hidden">
<div className="flex items-center justify-between px-5 py-3 border-b border-[rgba(2,42,2,0.08)]"> <div className="flex items-center justify-between px-5 py-3 border-b border-[rgba(2,42,2,0.08)]">
<h3 className="text-base font-medium text-[#141E12]">Certifications</h3> <h3 className="text-base font-medium text-[#141E12]">Certifications</h3>
<Button <Button
onClick={() => {}} // Will open modal to add custom certifications onClick={() => setShowAddModal(true)}
className="rounded-full bg-[rgba(0,39,0,0.05)] text-[#141E12] hover:bg-[rgba(0,39,0,0.08)] h-8 px-3" className="rounded-full bg-[rgba(0,39,0,0.05)] text-[#141E12] hover:bg-[rgba(0,39,0,0.08)] h-8 px-3"
size="sm" size="sm"
> >
@@ -498,139 +464,164 @@ function CertificationsView({
<div className="p-5 space-y-6"> <div className="p-5 space-y-6">
{/* Security Standards */} {/* Security Standards */}
<div className="space-y-3"> {securityCerts.length > 0 && (
<h4 className="text-sm font-medium text-[#141E12]">Security Standards</h4> <div className="space-y-3">
<div className="flex flex-wrap gap-2"> <h4 className="text-sm font-medium text-[#141E12]">Security Standards</h4>
{securityStandards.map(cert => ( <div className="flex flex-wrap gap-2">
<button {securityCerts.map(cert => (
key={cert} <div
onClick={() => isCertificationActive(cert) ? onRemove(cert) : onAdd(cert)} key={cert}
className={`flex items-center py-1.5 px-2 rounded text-sm font-medium ${ className="flex items-center gap-1 rounded bg-[rgba(5,77,5,0.03)] px-2 py-1.5 text-sm font-medium text-[#6B716A]"
isCertificationActive(cert) >
? "bg-[rgba(5,77,5,0.03)] text-[#6B716A]" <span>{cert}</span>
: "bg-[rgba(0,39,0,0.05)] text-[#141E12]" <button
}`} onClick={() => onRemove(cert)}
> className="text-[#6B716A] hover:text-[#141E12]"
{cert} >
{isCertificationActive(cert) && ( <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className="ml-1"> <path d="M12 4L4 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M12 4L4 12" stroke="#6B716A" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/> <path d="M4 4L12 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M4 4L12 12" stroke="#6B716A" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/> </svg>
</svg> </button>
)} </div>
</button> ))}
))} </div>
</div> </div>
</div> )}
{/* Regulatory & Legal */} {/* Regulatory & Legal */}
<div className="space-y-3"> {regulatoryCerts.length > 0 && (
<h4 className="text-sm font-medium text-[#141E12]">Regulatory & Legal</h4> <div className="space-y-3">
<div className="flex flex-wrap gap-2"> <h4 className="text-sm font-medium text-[#141E12]">Regulatory & Legal</h4>
{regulatoryLegal.map(cert => ( <div className="flex flex-wrap gap-2">
<button {regulatoryCerts.map(cert => (
key={cert} <div
onClick={() => isCertificationActive(cert) ? onRemove(cert) : onAdd(cert)} key={cert}
className={`flex items-center py-1.5 px-2 rounded text-sm font-medium ${ className="flex items-center gap-1 rounded bg-[rgba(5,77,5,0.03)] px-2 py-1.5 text-sm font-medium text-[#6B716A]"
isCertificationActive(cert) >
? "bg-[rgba(5,77,5,0.03)] text-[#6B716A]" <span>{cert}</span>
: "bg-[rgba(0,39,0,0.05)] text-[#141E12]" <button
}`} onClick={() => onRemove(cert)}
> className="text-[#6B716A] hover:text-[#141E12]"
{cert} >
{isCertificationActive(cert) && ( <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className="ml-1"> <path d="M12 4L4 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M12 4L4 12" stroke="#6B716A" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/> <path d="M4 4L12 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M4 4L12 12" stroke="#6B716A" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/> </svg>
</svg> </button>
)} </div>
</button> ))}
))} </div>
</div> </div>
</div> )}
{/* Industry-Specific */} {/* Industry-Specific */}
<div className="space-y-3"> {industryCerts.length > 0 && (
<h4 className="text-sm font-medium text-[#141E12]">Industry-Specific</h4> <div className="space-y-3">
<div className="flex flex-wrap gap-2"> <h4 className="text-sm font-medium text-[#141E12]">Industry-Specific</h4>
{industrySpecific.map(cert => ( <div className="flex flex-wrap gap-2">
<button {industryCerts.map(cert => (
key={cert} <div
onClick={() => isCertificationActive(cert) ? onRemove(cert) : onAdd(cert)} key={cert}
className={`flex items-center py-1.5 px-2 rounded text-sm font-medium ${ className="flex items-center gap-1 rounded bg-[rgba(5,77,5,0.03)] px-2 py-1.5 text-sm font-medium text-[#6B716A]"
isCertificationActive(cert) >
? "bg-[rgba(5,77,5,0.03)] text-[#6B716A]" <span>{cert}</span>
: "bg-[rgba(0,39,0,0.05)] text-[#141E12]" <button
}`} onClick={() => onRemove(cert)}
> className="text-[#6B716A] hover:text-[#141E12]"
{cert} >
{isCertificationActive(cert) && ( <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className="ml-1"> <path d="M12 4L4 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M12 4L4 12" stroke="#6B716A" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/> <path d="M4 4L12 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M4 4L12 12" stroke="#6B716A" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/> </svg>
</svg> </button>
)} </div>
</button> ))}
))} </div>
</div> </div>
</div> )}
{/* International & Government */} {/* International & Government */}
<div className="space-y-3"> {internationalCerts.length > 0 && (
<h4 className="text-sm font-medium text-[#141E12]">International & Government</h4> <div className="space-y-3">
<div className="flex flex-wrap gap-2"> <h4 className="text-sm font-medium text-[#141E12]">International & Government</h4>
{internationalGov.map(cert => ( <div className="flex flex-wrap gap-2">
<button {internationalCerts.map(cert => (
key={cert} <div
onClick={() => isCertificationActive(cert) ? onRemove(cert) : onAdd(cert)} key={cert}
className={`flex items-center py-1.5 px-2 rounded text-sm font-medium ${ className="flex items-center gap-1 rounded bg-[rgba(5,77,5,0.03)] px-2 py-1.5 text-sm font-medium text-[#6B716A]"
isCertificationActive(cert) >
? "bg-[rgba(5,77,5,0.03)] text-[#6B716A]" <span>{cert}</span>
: "bg-[rgba(0,39,0,0.05)] text-[#141E12]" <button
}`} onClick={() => onRemove(cert)}
> className="text-[#6B716A] hover:text-[#141E12]"
{cert} >
{isCertificationActive(cert) && ( <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className="ml-1"> <path d="M12 4L4 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M12 4L4 12" stroke="#6B716A" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/> <path d="M4 4L12 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M4 4L12 12" stroke="#6B716A" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/> </svg>
</svg> </button>
)} </div>
</button> ))}
))} </div>
</div> </div>
</div> )}
{/* Custom certifications */} {/* Custom certifications */}
{certifications.length > 0 && ( {customCerts.length > 0 && (
<div className="space-y-3"> <div className="space-y-3">
<h4 className="text-sm font-medium text-[#141E12]">Custom Certifications</h4> <h4 className="text-sm font-medium text-[#141E12]">Custom Certifications</h4>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{certifications {customCerts.map(cert => (
.filter(cert => <div
![...securityStandards, ...regulatoryLegal, ...industrySpecific, ...internationalGov].includes(cert) key={cert}
) className="flex items-center gap-1 rounded bg-[rgba(5,77,5,0.03)] px-2 py-1.5 text-sm font-medium text-[#6B716A]"
.map(cert => ( >
<div <span>{cert}</span>
key={cert} <button
className="flex items-center gap-1 rounded bg-[rgba(5,77,5,0.03)] px-2 py-1.5 text-sm font-medium text-[#6B716A]" onClick={() => onRemove(cert)}
className="text-[#6B716A] hover:text-[#141E12]"
> >
<span>{cert}</span> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<button <path d="M12 4L4 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
onClick={() => onRemove(cert)} <path d="M4 4L12 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
className="text-[#6B716A] hover:text-[#141E12]" </svg>
> </button>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> </div>
<path d="M12 4L4 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/> ))}
<path d="M4 4L12 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
</div>
))}
</div> </div>
</div> </div>
)} )}
</div> </div>
{/* Add Certification Modal */}
{showAddModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl p-6 w-full max-w-md">
<h3 className="text-lg font-medium mb-4">Add Custom Certification</h3>
<div className="space-y-4">
<div>
<Label htmlFor="certification-name">Certification Name</Label>
<Input
id="certification-name"
value={newCertification}
onChange={(e) => setNewCertification(e.target.value)}
placeholder="Enter certification name"
className="mt-1"
/>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setShowAddModal(false)}>
Cancel
</Button>
<Button onClick={handleAddCertification}>
Add
</Button>
</div>
</div>
</div>
</div>
)}
</div> </div>
); );
} }
@@ -935,6 +926,42 @@ function VendorViewContent({
setEditedFields((prev) => new Set(prev).add(field)); setEditedFields((prev) => new Set(prev).add(field));
}; };
// Auto-save handler specifically for certifications
const handleCertificationChange = useCallback((newCertifications: string[]) => {
// Update local state
setFormData(prev => ({
...prev,
certifications: newCertifications,
}));
// Auto-save the changes immediately
const formattedData = {
id: data.node.id!,
certifications: newCertifications,
};
updateVendor({
variables: {
input: formattedData,
},
onCompleted: () => {
toast({
title: "Success",
description: "Certifications updated",
variant: "default",
});
loadQuery({ vendorId: data.node.id!, organizationId: organizationId! });
},
onError: (error) => {
toast({
title: "Error",
description: error.message || "Failed to update certifications",
variant: "destructive",
});
},
});
}, [updateVendor, data.node.id, loadQuery, toast, organizationId]);
const handleCancel = () => { const handleCancel = () => {
setFormData({ setFormData({
name: data.node.name || "", name: data.node.name || "",
@@ -1378,14 +1405,13 @@ function VendorViewContent({
<CertificationsView <CertificationsView
certifications={[...formData.certifications]} certifications={[...formData.certifications]}
onAdd={(cert) => onAdd={(cert) =>
handleFieldChange("certifications", [ handleCertificationChange([
...formData.certifications.filter(c => c !== cert), ...formData.certifications.filter(c => c !== cert),
cert, cert,
]) ])
} }
onRemove={(cert) => onRemove={(cert) =>
handleFieldChange( handleCertificationChange(
"certifications",
formData.certifications.filter((c) => c !== cert) formData.certifications.filter((c) => c !== cert)
) )
} }