Fetch vendor data with openai

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-05-26 10:45:10 -07:00
parent 8f41461bd1
commit c2f9cb2080
15 changed files with 1044 additions and 25 deletions

View File

@@ -20,6 +20,7 @@ import type { VendorViewDeleteComplianceReportMutation as DeleteComplianceReport
import type { VendorViewUploadComplianceReportMutation as UploadComplianceReportMutationType } from "./__generated__/VendorViewUploadComplianceReportMutation.graphql";
import type { VendorViewUpdateVendorMutation } from "./__generated__/VendorViewUpdateVendorMutation.graphql";
import type { VendorViewCreateRiskAssessmentMutation } from "./__generated__/VendorViewCreateRiskAssessmentMutation.graphql";
import type { VendorViewAssessVendorMutation } from "./__generated__/VendorViewAssessVendorMutation.graphql";
import type {
BusinessImpact,
DataSensitivity,
@@ -31,6 +32,7 @@ import { PageTemplate } from "@/components/PageTemplate";
import { VendorViewSkeleton } from "./VendorPage";
import PeopleSelector from "@/components/PeopleSelector";
import { formatDistanceToNow, format, isPast } from "date-fns";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
const vendorViewQuery = graphql`
query VendorViewQuery($vendorId: ID!, $organizationId: ID!) {
@@ -196,6 +198,38 @@ const createRiskAssessmentMutation = graphql`
}
`;
const assessVendorMutation = graphql`
mutation VendorViewAssessVendorMutation($input: AssessVendorInput!) {
assessVendor(input: $input) {
vendor {
id
name
description
statusPageUrl
termsOfServiceUrl
privacyPolicyUrl
serviceLevelAgreementUrl
dataProcessingAgreementUrl
securityPageUrl
trustPageUrl
certifications
headquarterAddress
legalName
websiteUrl
businessOwner {
id
fullName
}
securityOwner {
id
fullName
}
updatedAt
}
}
}
`;
// Format date for input field (YYYY-MM-DDTHH:mm)
function formatDateForInput(date: string | null | undefined): string {
if (!date) return "";
@@ -1070,9 +1104,9 @@ function RiskAssessmentDetailsModal({
<div className="bg-white rounded-xl p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium">Risk Assessment Details</h3>
<Button
variant="ghost"
size="icon"
<Button
variant="ghost"
size="icon"
onClick={onClose}
className="h-8 w-8 rounded-full"
>
@@ -1092,7 +1126,7 @@ function RiskAssessmentDetailsModal({
{formatDate(assessment.assessedAt || "")}
</p>
</div>
<div>
<h4 className="text-sm font-medium text-[#6B716A] mb-1">Valid Until</h4>
<p className="text-sm font-medium">
@@ -1140,7 +1174,7 @@ function RiskAssessmentDetailsModal({
)}
<div className="flex justify-end mt-4">
<Button
<Button
onClick={onClose}
className="bg-[#054D05] text-white hover:bg-[#054D05]/90"
>
@@ -1398,6 +1432,13 @@ function VendorViewContent({
);
const [, loadQuery] = useQueryLoader<VendorViewQueryType>(vendorViewQuery);
const { toast } = useToast();
const [isAssessDialogOpen, setIsAssessDialogOpen] = useState(false);
const [websiteUrl, setWebsiteUrl] = useState("");
const [commitAssessVendor, isInFlight] = useMutation<VendorViewAssessVendorMutation>(
assessVendorMutation
);
const hasChanges = editedFields.size > 0;
@@ -2301,8 +2342,92 @@ function VendorViewContent({
}
};
const handleAssessVendor = useCallback(() => {
if (!websiteUrl) {
toast({
title: "Error",
description: "Please enter a website URL",
variant: "destructive",
});
return;
}
commitAssessVendor({
variables: {
input: {
id: data.node.id!,
websiteUrl,
},
},
onCompleted: (response) => {
const newData = response.assessVendor.vendor;
const changedFields = new Set<string>();
// Compare and track changes
Object.entries(newData).forEach(([key, value]) => {
if (value != null && key !== 'id' && formData[key as keyof typeof formData] !== value) {
changedFields.add(key);
}
});
setFormData(prevData => {
const newFormData = {
...prevData,
name: newData.name || prevData.name,
description: newData.description || prevData.description,
statusPageUrl: newData.statusPageUrl || prevData.statusPageUrl,
termsOfServiceUrl: newData.termsOfServiceUrl || prevData.termsOfServiceUrl,
privacyPolicyUrl: newData.privacyPolicyUrl || prevData.privacyPolicyUrl,
serviceLevelAgreementUrl: newData.serviceLevelAgreementUrl || prevData.serviceLevelAgreementUrl,
dataProcessingAgreementUrl: newData.dataProcessingAgreementUrl || prevData.dataProcessingAgreementUrl,
securityPageUrl: newData.securityPageUrl || prevData.securityPageUrl,
trustPageUrl: newData.trustPageUrl || prevData.trustPageUrl,
certifications: newData.certifications || prevData.certifications,
headquarterAddress: newData.headquarterAddress || prevData.headquarterAddress,
legalName: newData.legalName || prevData.legalName,
websiteUrl: newData.websiteUrl || prevData.websiteUrl,
};
return newFormData;
});
setEditedFields(prev => {
const newSet = new Set(prev);
changedFields.forEach(field => newSet.add(field));
return newSet;
});
toast({
title: "Success",
description: changedFields.size > 0
? "Vendor has been assessed. Review and save the changes."
: "Vendor data has been assessed. No changes were found.",
});
setIsAssessDialogOpen(false);
setWebsiteUrl("");
},
onError: (error) => {
toast({
title: "Error",
description: error.message,
variant: "destructive",
});
},
});
}, [commitAssessVendor, data.node.id, websiteUrl, toast, formData]);
return (
<PageTemplate title={formData.name}>
<PageTemplate
title={formData.name}
actions={
<Button
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"
onClick={() => setIsAssessDialogOpen(true)}
>
Assessement From Website
</Button>
}
>
<div className="border-b mb-6">
<div className="flex">
<button
@@ -2377,12 +2502,71 @@ function VendorViewContent({
onSubmit={handleRiskAssessmentSubmit}
businessOwnerId={formData.businessOwnerId}
/>
{/* Assessement From Website Modal */}
<Dialog open={isAssessDialogOpen} onOpenChange={setIsAssessDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Assessement From Website</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<Input
placeholder="Enter website URL"
value={websiteUrl}
onChange={(e) => setWebsiteUrl(e.target.value)}
disabled={isInFlight}
/>
{isInFlight && (
<div className="flex items-center justify-center text-sm text-[#6B716A]">
<svg
className="animate-spin -ml-1 mr-3 h-5 w-5 text-[#054D05]"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Assessing vendor data...
</div>
)}
</div>
<div className="flex justify-end gap-2">
<Button
variant="outline"
onClick={() => setIsAssessDialogOpen(false)}
disabled={isInFlight}
>
Cancel
</Button>
<Button
onClick={handleAssessVendor}
disabled={isInFlight}
className={isInFlight ? "opacity-50 cursor-not-allowed" : ""}
>
{isInFlight ? "Processing..." : "Assess"}
</Button>
</div>
</DialogContent>
</Dialog>
</PageTemplate>
);
}
export default function VendorView() {
const { vendorId, organizationId } = useParams();
const [queryRef, loadQuery] =
useQueryLoader<VendorViewQueryType>(vendorViewQuery);

View File

@@ -0,0 +1,257 @@
/**
* @generated SignedSource<<71c7ed1be8cf0477074d6cf130143626>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type AssessVendorInput = {
id: string;
websiteUrl: string;
};
export type VendorViewAssessVendorMutation$variables = {
input: AssessVendorInput;
};
export type VendorViewAssessVendorMutation$data = {
readonly assessVendor: {
readonly vendor: {
readonly businessOwner: {
readonly fullName: string;
readonly id: string;
} | null | undefined;
readonly certifications: ReadonlyArray<string>;
readonly dataProcessingAgreementUrl: string | null | undefined;
readonly description: string | null | undefined;
readonly headquarterAddress: string | null | undefined;
readonly id: string;
readonly legalName: string | null | undefined;
readonly name: string;
readonly privacyPolicyUrl: string | null | undefined;
readonly securityOwner: {
readonly fullName: string;
readonly id: string;
} | null | undefined;
readonly securityPageUrl: string | null | undefined;
readonly serviceLevelAgreementUrl: string | null | undefined;
readonly statusPageUrl: string | null | undefined;
readonly termsOfServiceUrl: string | null | undefined;
readonly trustPageUrl: string | null | undefined;
readonly updatedAt: string;
readonly websiteUrl: string | null | undefined;
};
};
};
export type VendorViewAssessVendorMutation = {
response: VendorViewAssessVendorMutation$data;
variables: VendorViewAssessVendorMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v2 = [
(v1/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
}
],
v3 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "AssessVendorPayload",
"kind": "LinkedField",
"name": "assessVendor",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Vendor",
"kind": "LinkedField",
"name": "vendor",
"plural": false,
"selections": [
(v1/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "statusPageUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "termsOfServiceUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "privacyPolicyUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "serviceLevelAgreementUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "dataProcessingAgreementUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "securityPageUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "trustPageUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "certifications",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "headquarterAddress",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "legalName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "websiteUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "businessOwner",
"plural": false,
"selections": (v2/*: any*/),
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "securityOwner",
"plural": false,
"selections": (v2/*: any*/),
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "VendorViewAssessVendorMutation",
"selections": (v3/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "VendorViewAssessVendorMutation",
"selections": (v3/*: any*/)
},
"params": {
"cacheID": "ccc16b2de8f7f3936458cd4823a14992",
"id": null,
"metadata": {},
"name": "VendorViewAssessVendorMutation",
"operationKind": "mutation",
"text": "mutation VendorViewAssessVendorMutation(\n $input: AssessVendorInput!\n) {\n assessVendor(input: $input) {\n vendor {\n id\n name\n description\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n serviceLevelAgreementUrl\n dataProcessingAgreementUrl\n securityPageUrl\n trustPageUrl\n certifications\n headquarterAddress\n legalName\n websiteUrl\n businessOwner {\n id\n fullName\n }\n securityOwner {\n id\n fullName\n }\n updatedAt\n }\n }\n}\n"
}
};
})();
(node as any).hash = "ed23d00ce5a44aa6bfd43a072552ed1c";
export default node;