11import { logger } from '@/lib/logger' ;
2- import { useState , useMemo , useCallback } from 'react' ;
2+ import { useState , useMemo , useCallback , useEffect } from 'react' ;
33import { useQueryClient } from '@tanstack/react-query' ;
44import { StarterTemplate } from '@/data/templates' ;
55import { TemplateOverwriteOptions } from '@/components/admin/templates/TemplatePreviewDialog' ;
@@ -25,10 +25,33 @@ export interface InstallProgress {
2525 currentStep : string ;
2626}
2727
28+ export interface TemplateManifest {
29+ pageIds : string [ ] ;
30+ blogPostIds : string [ ] ;
31+ kbCategoryIds : string [ ] ;
32+ productIds : string [ ] ;
33+ consultantIds : string [ ] ;
34+ }
35+
2836export function useTemplateInstaller ( ) {
2937 const [ step , setStep ] = useState < InstallStep > ( 'idle' ) ;
3038 const [ progress , setProgress ] = useState < InstallProgress > ( { currentPage : 0 , totalPages : 0 , currentStep : '' } ) ;
3139 const [ createdPageIds , setCreatedPageIds ] = useState < string [ ] > ( [ ] ) ;
40+ const [ installedTemplate , setInstalledTemplate ] = useState < { template_id : string ; template_name : string ; manifest : TemplateManifest } | null > ( null ) ;
41+
42+ // Fetch currently installed template on mount
43+ useEffect ( ( ) => {
44+ supabase . from ( 'installed_template' ) . select ( '*' ) . order ( 'installed_at' , { ascending : false } ) . limit ( 1 )
45+ . then ( ( { data } ) => {
46+ if ( data && data . length > 0 ) {
47+ setInstalledTemplate ( {
48+ template_id : data [ 0 ] . template_id ,
49+ template_name : data [ 0 ] . template_name ,
50+ manifest : data [ 0 ] . manifest as unknown as TemplateManifest ,
51+ } ) ;
52+ }
53+ } ) ;
54+ } , [ ] ) ;
3255
3356 const { data : existingPages } = usePages ( ) ;
3457 const { data : deletedPages } = useDeletedPages ( ) ;
@@ -244,12 +267,71 @@ export function useTemplateInstaller() {
244267 await updateModules . mutateAsync ( updatedModules ) ;
245268 }
246269
247- // Delete existing pages
248- if ( opts . pages && existingPages && existingPages . length > 0 ) {
249- setProgress ( { currentPage : 0 , totalPages : existingPages . length , currentStep : 'Clearing existing pages...' } ) ;
250- for ( let i = 0 ; i < existingPages . length ; i ++ ) {
251- setProgress ( { currentPage : i + 1 , totalPages : existingPages . length , currentStep : `Removing page "${ existingPages [ i ] . title } "...` } ) ;
252- await permanentDeletePage . mutateAsync ( existingPages [ i ] . id ) ;
270+ // Auto-cleanup previous template using manifest
271+ if ( installedTemplate ?. manifest ) {
272+ const m = installedTemplate . manifest ;
273+ const totalCleanup = ( m . pageIds ?. length || 0 ) + ( m . blogPostIds ?. length || 0 ) + ( m . kbCategoryIds ?. length || 0 ) + ( m . productIds ?. length || 0 ) + ( m . consultantIds ?. length || 0 ) ;
274+ if ( totalCleanup > 0 ) {
275+ setProgress ( { currentPage : 0 , totalPages : totalCleanup , currentStep : `Uninstalling "${ installedTemplate . template_name } "...` } ) ;
276+ let cleaned = 0 ;
277+
278+ // Remove pages created by previous template
279+ for ( const pageId of ( m . pageIds || [ ] ) ) {
280+ setProgress ( { currentPage : ++ cleaned , totalPages : totalCleanup , currentStep : 'Removing previous template pages...' } ) ;
281+ try { await permanentDeletePage . mutateAsync ( pageId ) ; } catch { /* already deleted */ }
282+ }
283+
284+ // Remove blog posts
285+ for ( const postId of ( m . blogPostIds || [ ] ) ) {
286+ setProgress ( { currentPage : ++ cleaned , totalPages : totalCleanup , currentStep : 'Removing previous template blog posts...' } ) ;
287+ try { await deleteBlogPost . mutateAsync ( postId ) ; } catch { /* already deleted */ }
288+ }
289+
290+ // Remove KB categories
291+ for ( const catId of ( m . kbCategoryIds || [ ] ) ) {
292+ setProgress ( { currentPage : ++ cleaned , totalPages : totalCleanup , currentStep : 'Removing previous template KB content...' } ) ;
293+ try { await deleteKbCategory . mutateAsync ( catId ) ; } catch { /* already deleted */ }
294+ }
295+
296+ // Remove products
297+ for ( const prodId of ( m . productIds || [ ] ) ) {
298+ setProgress ( { currentPage : ++ cleaned , totalPages : totalCleanup , currentStep : 'Removing previous template products...' } ) ;
299+ try { await deleteProduct . mutateAsync ( prodId ) ; } catch { /* already deleted */ }
300+ }
301+
302+ // Remove consultants
303+ for ( const conId of ( m . consultantIds || [ ] ) ) {
304+ setProgress ( { currentPage : ++ cleaned , totalPages : totalCleanup , currentStep : 'Removing previous template consultants...' } ) ;
305+ try { await supabase . from ( 'consultant_profiles' ) . delete ( ) . eq ( 'id' , conId ) ; } catch { /* already deleted */ }
306+ }
307+
308+ // Remove old manifest record
309+ await supabase . from ( 'installed_template' ) . delete ( ) . neq ( 'id' , '00000000-0000-0000-0000-000000000000' ) ;
310+ logger . log ( `[TemplateInstaller] Uninstalled previous template "${ installedTemplate . template_name } " (${ totalCleanup } resources)` ) ;
311+ }
312+ } else {
313+ // No manifest — fall back to clearing all existing content (first install or legacy)
314+ if ( opts . pages && existingPages && existingPages . length > 0 ) {
315+ setProgress ( { currentPage : 0 , totalPages : existingPages . length , currentStep : 'Clearing existing pages...' } ) ;
316+ for ( let i = 0 ; i < existingPages . length ; i ++ ) {
317+ setProgress ( { currentPage : i + 1 , totalPages : existingPages . length , currentStep : `Removing page "${ existingPages [ i ] . title } "...` } ) ;
318+ await permanentDeletePage . mutateAsync ( existingPages [ i ] . id ) ;
319+ }
320+ }
321+ if ( opts . blogPosts && existingBlogPosts && existingBlogPosts . length > 0 ) {
322+ for ( let i = 0 ; i < existingBlogPosts . length ; i ++ ) {
323+ await deleteBlogPost . mutateAsync ( existingBlogPosts [ i ] . id ) ;
324+ }
325+ }
326+ if ( opts . kbContent && existingKbCategories && existingKbCategories . length > 0 ) {
327+ for ( let i = 0 ; i < existingKbCategories . length ; i ++ ) {
328+ await deleteKbCategory . mutateAsync ( existingKbCategories [ i ] . id ) ;
329+ }
330+ }
331+ if ( opts . products && existingProducts && existingProducts . length > 0 ) {
332+ for ( let i = 0 ; i < existingProducts . length ; i ++ ) {
333+ await deleteProduct . mutateAsync ( existingProducts [ i ] . id ) ;
334+ }
253335 }
254336 }
255337
@@ -258,58 +340,10 @@ export function useTemplateInstaller() {
258340 const templateSlugs = new Set ( template . pages . map ( p => p . slug ) ) ;
259341 const conflicting = deletedPages . filter ( p => templateSlugs . has ( p . slug ) ) ;
260342 for ( const page of conflicting ) {
261- setProgress ( { currentPage : 0 , totalPages : conflicting . length , currentStep : `Cleaning up trashed page "${ page . title } "...` } ) ;
262- await permanentDeletePage . mutateAsync ( page . id ) ;
263- }
264- }
265-
266- // Create pages
267- if ( opts . pages ) {
268- setProgress ( { currentPage : 0 , totalPages : templatePages . length , currentStep : 'Creating pages...' } ) ;
269- for ( let i = 0 ; i < templatePages . length ; i ++ ) {
270- const templatePage = templatePages [ i ] ;
271- setProgress ( { currentPage : i + 1 , totalPages : templatePages . length , currentStep : `Creating "${ templatePage . title } "...` } ) ;
272- const page = await createPage . mutateAsync ( {
273- title : templatePage . title ,
274- slug : templatePage . slug ,
275- content : templatePage . blocks ,
276- meta : templatePage . meta ,
277- menu_order : templatePage . menu_order ,
278- show_in_menu : templatePage . showInMenu ,
279- status : opts . publishPages ? 'published' : 'draft' ,
280- } ) ;
281- pageIds . push ( page . id ) ;
343+ try { await permanentDeletePage . mutateAsync ( page . id ) ; } catch { /* already deleted */ }
282344 }
283345 }
284346
285- // Delete existing blog posts
286- if ( opts . blogPosts && existingBlogPosts && existingBlogPosts . length > 0 ) {
287- setProgress ( { currentPage : 0 , totalPages : existingBlogPosts . length , currentStep : 'Clearing existing blog posts...' } ) ;
288- for ( let i = 0 ; i < existingBlogPosts . length ; i ++ ) {
289- setProgress ( { currentPage : i + 1 , totalPages : existingBlogPosts . length , currentStep : `Removing blog post "${ existingBlogPosts [ i ] . title } "...` } ) ;
290- await deleteBlogPost . mutateAsync ( existingBlogPosts [ i ] . id ) ;
291- }
292- }
293-
294- // Delete existing KB categories
295- if ( opts . kbContent && existingKbCategories && existingKbCategories . length > 0 ) {
296- setProgress ( { currentPage : 0 , totalPages : existingKbCategories . length , currentStep : 'Clearing existing KB content...' } ) ;
297- for ( let i = 0 ; i < existingKbCategories . length ; i ++ ) {
298- setProgress ( { currentPage : i + 1 , totalPages : existingKbCategories . length , currentStep : `Removing KB category "${ existingKbCategories [ i ] . name } "...` } ) ;
299- await deleteKbCategory . mutateAsync ( existingKbCategories [ i ] . id ) ;
300- }
301- }
302-
303- // Delete existing products
304- if ( opts . products && existingProducts && existingProducts . length > 0 ) {
305- setProgress ( { currentPage : 0 , totalPages : existingProducts . length , currentStep : 'Clearing existing products...' } ) ;
306- for ( let i = 0 ; i < existingProducts . length ; i ++ ) {
307- setProgress ( { currentPage : i + 1 , totalPages : existingProducts . length , currentStep : `Removing product "${ existingProducts [ i ] . name } "...` } ) ;
308- await deleteProduct . mutateAsync ( existingProducts [ i ] . id ) ;
309- }
310- }
311-
312- // Apply branding
313347 if ( opts . branding ) {
314348 setProgress ( { currentPage : 0 , totalPages : 1 , currentStep : 'Applying branding...' } ) ;
315349 await updateBranding . mutateAsync ( template . branding ) ;
@@ -361,12 +395,13 @@ export function useTemplateInstaller() {
361395 }
362396
363397 // Create products
398+ const createdProductIds : string [ ] = [ ] ;
364399 if ( opts . products ) {
365400 const productsToCreate = templateProducts || [ ] ;
366401 for ( let i = 0 ; i < productsToCreate . length ; i ++ ) {
367402 const product = productsToCreate [ i ] ;
368403 setProgress ( { currentPage : i + 1 , totalPages : productsToCreate . length , currentStep : `Creating product "${ product . name } "...` } ) ;
369- await createProduct . mutateAsync ( {
404+ const created = await createProduct . mutateAsync ( {
370405 name : product . name ,
371406 description : product . description ,
372407 price_cents : product . price_cents ,
@@ -377,42 +412,48 @@ export function useTemplateInstaller() {
377412 sort_order : i ,
378413 stripe_price_id : null ,
379414 } ) ;
415+ if ( created ?. id ) createdProductIds . push ( created . id ) ;
380416 }
381417 }
382418
383419 // Seed consultant profiles
420+ const createdConsultantIds : string [ ] = [ ] ;
384421 if ( template . consultants ?. length ) {
385422 const consultants = template . consultants ;
386423 setProgress ( { currentPage : 0 , totalPages : consultants . length , currentStep : 'Seeding consultant profiles...' } ) ;
387424 for ( let i = 0 ; i < consultants . length ; i ++ ) {
388425 const c = consultants [ i ] ;
389426 setProgress ( { currentPage : i + 1 , totalPages : consultants . length , currentStep : `Adding consultant "${ c . name } "...` } ) ;
390- await supabase . from ( 'consultant_profiles' ) . insert ( {
427+ const { data } = await supabase . from ( 'consultant_profiles' ) . insert ( {
391428 name : c . name , title : c . title , summary : c . summary , bio : c . bio || null ,
392429 skills : c . skills , experience_years : c . experience_years ,
393430 certifications : c . certifications || [ ] , languages : c . languages || [ 'English' ] ,
394431 availability : c . availability , hourly_rate_cents : c . hourly_rate_cents || null ,
395432 currency : c . currency || 'USD' , avatar_url : c . avatar_url || null ,
396433 linkedin_url : c . linkedin_url || null , is_active : c . is_active ?? true ,
397- } ) ;
434+ } ) . select ( 'id' ) . single ( ) ;
435+ if ( data ?. id ) createdConsultantIds . push ( data . id ) ;
398436 }
399437 }
400438
401439 // Create blog posts
440+ const createdBlogPostIds : string [ ] = [ ] ;
402441 if ( opts . blogPosts ) {
403442 const postsToCreate = templateBlogPosts || [ ] ;
404443 for ( let i = 0 ; i < postsToCreate . length ; i ++ ) {
405444 const post = postsToCreate [ i ] ;
406445 setProgress ( { currentPage : i + 1 , totalPages : postsToCreate . length , currentStep : `Creating blog post "${ post . title } "...` } ) ;
407- await createBlogPost . mutateAsync ( {
446+ const created = await createBlogPost . mutateAsync ( {
408447 title : post . title , slug : post . slug , excerpt : post . excerpt ,
409448 featured_image : post . featured_image , content : post . content ,
410449 meta : post . meta , status : opts . publishBlogPosts ? 'published' : 'draft' ,
411450 } ) ;
451+ if ( created ?. id ) createdBlogPostIds . push ( created . id ) ;
412452 }
413453 }
414454
415455 // Create KB categories and articles
456+ const createdKbCategoryIds : string [ ] = [ ] ;
416457 let totalKbArticles = 0 ;
417458 if ( opts . kbContent ) {
418459 const kbCategories = template . kbCategories || [ ] ;
@@ -423,6 +464,7 @@ export function useTemplateInstaller() {
423464 name : category . name , slug : category . slug , description : category . description ,
424465 icon : category . icon , is_active : true ,
425466 } ) ;
467+ createdKbCategoryIds . push ( createdCategory . id ) ;
426468 for ( const article of category . articles ) {
427469 const answerJson = createDocumentFromText ( article . answer_text ) ;
428470 await createKbArticle . mutateAsync ( {
@@ -454,6 +496,23 @@ export function useTemplateInstaller() {
454496 logger . warn ( '[TemplateInstaller] FlowPilot bootstrap failed (non-fatal):' , fpError ) ;
455497 }
456498
499+ // Save installation manifest for future cleanup
500+ const manifest : TemplateManifest = {
501+ pageIds,
502+ blogPostIds : createdBlogPostIds ,
503+ kbCategoryIds : createdKbCategoryIds ,
504+ productIds : createdProductIds ,
505+ consultantIds : createdConsultantIds ,
506+ } ;
507+ await supabase . from ( 'installed_template' ) . delete ( ) . neq ( 'id' , '00000000-0000-0000-0000-000000000000' ) ;
508+ await supabase . from ( 'installed_template' ) . insert ( {
509+ template_id : template . id ,
510+ template_name : template . name ,
511+ manifest : manifest as any ,
512+ } ) ;
513+ setInstalledTemplate ( { template_id : template . id , template_name : template . name , manifest } ) ;
514+ logger . log ( '[TemplateInstaller] Saved manifest:' , manifest ) ;
515+
457516 setCreatedPageIds ( pageIds ) ;
458517 setStep ( 'done' ) ;
459518
@@ -481,7 +540,7 @@ export function useTemplateInstaller() {
481540 toast ( { title : 'Error' , description : 'Failed to apply template. Some changes may have been applied.' , variant : 'destructive' } ) ;
482541 setStep ( 'idle' ) ;
483542 }
484- } , [ existingPages , deletedPages , existingBlogPosts , existingKbCategories , existingProducts , mediaCount , currentModules ] ) ;
543+ } , [ existingPages , deletedPages , existingBlogPosts , existingKbCategories , existingProducts , mediaCount , currentModules , installedTemplate ] ) ;
485544
486545 const reset = useCallback ( ( ) => {
487546 setStep ( 'idle' ) ;
@@ -500,6 +559,7 @@ export function useTemplateInstaller() {
500559 createdPageIds,
501560 existingContent,
502561 hasExistingContent,
562+ installedTemplate,
503563 install,
504564 reset,
505565 } ;
0 commit comments