Fix certification update
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
@@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user