From 7afbcfd9f5bc08f9ec4ec9b9dc690294adf4613d Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Wed, 11 Mar 2026 08:33:12 +0100 Subject: [PATCH] fix(sdk): Auto-save company profile to SDK context and backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Profile data was lost when navigating away because it was only saved to SDK context on explicit button click (Next/Save). Scope data persisted because it auto-synced on every change. Added two debounced auto-save mechanisms: - SDK context sync (500ms) — survives in-app navigation - Backend save (2s) — survives page reload/session change Co-Authored-By: Claude Opus 4.6 --- .../app/sdk/company-profile/page.tsx | 69 ++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/admin-compliance/app/sdk/company-profile/page.tsx b/admin-compliance/app/sdk/company-profile/page.tsx index af6c26a..9c80ce5 100644 --- a/admin-compliance/app/sdk/company-profile/page.tsx +++ b/admin-compliance/app/sdk/company-profile/page.tsx @@ -1,6 +1,6 @@ 'use client' -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useRef } from 'react' import { useSDK } from '@/lib/sdk' import { CompanyProfile, @@ -2469,6 +2469,39 @@ export default function CompanyProfilePage() { setFormData(prev => ({ ...prev, ...updates })) } + // --------------------------------------------------------------------------- + // Auto-save: sync formData to SDK context (debounced) so data survives navigation. + // This mirrors the pattern used by compliance-scope/page.tsx. + // --------------------------------------------------------------------------- + const autoSaveRef = useRef | null>(null) + const initialLoadDone = useRef(false) + + useEffect(() => { + // Skip the initial load — only auto-save after user has started editing + if (!initialLoadDone.current) { + // Mark initial load done after first formData update (from backend or SDK state) + if (formData.companyName !== undefined) { + initialLoadDone.current = true + } + return + } + + // Debounce: sync to SDK context after 500ms of inactivity + if (autoSaveRef.current) clearTimeout(autoSaveRef.current) + autoSaveRef.current = setTimeout(() => { + // Only sync if there's meaningful data (not just defaults) + const hasData = formData.companyName || (formData.industry && formData.industry.length > 0) + if (hasData) { + setCompanyProfile({ ...formData, isComplete: false, completedAt: null } as CompanyProfile) + } + }, 500) + + return () => { + if (autoSaveRef.current) clearTimeout(autoSaveRef.current) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [formData]) + // Shared payload builder for draft saves and final save (DRY) const buildProfilePayload = (isComplete: boolean) => ({ project_id: projectId || null, @@ -2537,7 +2570,39 @@ export default function CompanyProfilePage() { } : {}), }) - // Auto-save draft to backend (fire-and-forget, non-blocking) + // --------------------------------------------------------------------------- + // Auto-save draft to backend (debounced, 2s after last change) + // --------------------------------------------------------------------------- + const backendAutoSaveRef = useRef | null>(null) + + useEffect(() => { + if (!initialLoadDone.current) return + + const hasData = formData.companyName || (formData.industry && formData.industry.length > 0) + if (!hasData) return + + if (backendAutoSaveRef.current) clearTimeout(backendAutoSaveRef.current) + backendAutoSaveRef.current = setTimeout(async () => { + try { + await fetch(profileApiUrl(), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(buildProfilePayload(false)), + }) + setDraftSaveStatus('saved') + if (draftSaveTimerRef.current) clearTimeout(draftSaveTimerRef.current) + draftSaveTimerRef.current = setTimeout(() => setDraftSaveStatus('idle'), 3000) + } catch { + // Silent fail for auto-save — user can still manually save via Next + } + }, 2000) + + return () => { + if (backendAutoSaveRef.current) clearTimeout(backendAutoSaveRef.current) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [formData]) + const [draftSaveStatus, setDraftSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle') const draftSaveTimerRef = React.useRef | null>(null)