import { type CSSProperties, useEffect, useState } from "react"; import { RestApiClient } from "twenty-client-sdk/rest"; import { defineFrontComponent } from "twenty-sdk/define"; import { closeSidePanel, enqueueSnackbar, unmountFrontComponent, useSelectedRecordIds, } from "twenty-sdk/front-component"; import { CREATE_DOCUMENT_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from "src/constants/universal-identifiers"; type Template = { id: string; name: string; }; type TemplatesResponse = { templates?: Template[]; error?: string; }; type CreateResponse = { success: boolean; message: string; sajnUrl?: string; sent?: boolean; }; const theme = { accent: "var(--t-color-blue)", background: "var(--t-background-primary)", backgroundSecondary: "var(--t-background-secondary)", border: "var(--t-border-color-medium)", borderLight: "var(--t-border-color-light)", font: "var(--t-font-family)", fontPrimary: "var(--t-font-color-primary)", fontSecondary: "var(--t-font-color-secondary)", fontTertiary: "var(--t-font-color-tertiary)", fontInverted: "var(--t-font-color-inverted)", radius: "var(--t-border-radius-sm)", spacing2: "var(--t-spacing-2)", spacing3: "var(--t-spacing-3)", spacing4: "var(--t-spacing-4)", spacing8: "var(--t-spacing-8)", }; const styles: Record = { container: { background: theme.background, color: theme.fontPrimary, display: "flex", flexDirection: "column", fontFamily: theme.font, height: "100%", }, header: { borderBottom: `1px solid ${theme.borderLight}`, padding: theme.spacing4, }, title: { fontSize: "16px", fontWeight: 600, margin: 0 }, subtitle: { color: theme.fontTertiary, fontSize: "13px", margin: "6px 0 0" }, form: { display: "flex", flex: 1, flexDirection: "column", gap: theme.spacing4, overflow: "auto", padding: theme.spacing4, }, field: { display: "flex", flexDirection: "column", gap: theme.spacing2 }, label: { color: theme.fontSecondary, fontSize: "12px", fontWeight: 500 }, input: { background: theme.backgroundSecondary, border: `1px solid ${theme.border}`, borderRadius: theme.radius, boxSizing: "border-box", color: theme.fontPrimary, fontFamily: theme.font, fontSize: "13px", minHeight: theme.spacing8, padding: `${theme.spacing2} ${theme.spacing3}`, width: "100%", }, helper: { color: theme.fontTertiary, fontSize: "11px" }, checkboxRow: { alignItems: "center", display: "flex", gap: theme.spacing2 }, footer: { borderTop: `1px solid ${theme.borderLight}`, display: "flex", gap: theme.spacing2, justifyContent: "flex-end", padding: theme.spacing3, }, button: { border: `1px solid ${theme.border}`, borderRadius: theme.radius, cursor: "pointer", fontFamily: theme.font, fontSize: "13px", minHeight: theme.spacing8, padding: `0 ${theme.spacing3}`, }, secondaryButton: { background: theme.backgroundSecondary, color: theme.fontSecondary, }, primaryButton: { background: theme.accent, borderColor: theme.accent, color: theme.fontInverted, }, }; const CreateSajnDocument = () => { const selectedRecordIds = useSelectedRecordIds(); const opportunityId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : undefined; const [templates, setTemplates] = useState([]); const [templateId, setTemplateId] = useState(""); const [documentName, setDocumentName] = useState(""); const [recipientName, setRecipientName] = useState(""); const [recipientEmail, setRecipientEmail] = useState(""); const [sendNow, setSendNow] = useState(false); const [loading, setLoading] = useState(true); const [submitting, setSubmitting] = useState(false); useEffect(() => { let cancelled = false; new RestApiClient() .get("/s/sajn/templates") .then((result) => { if (cancelled) { return; } const availableTemplates = result.templates ?? []; setTemplates(availableTemplates); setTemplateId(availableTemplates[0]?.id ?? ""); }) .catch(async (error: unknown) => { if (!cancelled) { await enqueueSnackbar({ message: error instanceof Error ? error.message : "Could not load sajn templates.", variant: "error", }); } }) .finally(() => { if (!cancelled) { setLoading(false); } }); return () => { cancelled = true; }; }, []); const close = () => { unmountFrontComponent(); closeSidePanel(); }; const submit = async () => { if (!opportunityId || !templateId || submitting) { return; } setSubmitting(true); try { const result = await new RestApiClient().post( "/s/sajn/documents", { opportunityId, templateId, requestId: crypto.randomUUID(), documentName: documentName || undefined, recipientName: recipientName || undefined, recipientEmail: recipientEmail || undefined, sendNow, }, ); await enqueueSnackbar({ message: result.message, variant: result.success ? "success" : "error", }); if (result.success) { close(); } } catch (error) { await enqueueSnackbar({ message: error instanceof Error ? error.message : "Could not create the sajn document.", variant: "error", }); } finally { setSubmitting(false); } }; const canSubmit = Boolean(opportunityId) && Boolean(templateId) && !loading && !submitting; return (

Create sajn document

The opportunity and its point of contact will prefill the document.

{ event.preventDefault(); void submit(); }} >
); }; export default defineFrontComponent({ universalIdentifier: CREATE_DOCUMENT_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER, name: "create-sajn-document", description: "Creates a sajn document from the selected opportunity.", component: CreateSajnDocument, });