33namespace App \Livewire \Projects \Partials ;
44
55use App \Enums \DocumentType ;
6+ use App \Enums \TemplateCategory ;
67use App \Models \Project ;
78use App \Models \Template ;
89use Illuminate \Database \Eloquent \Collection ;
@@ -22,6 +23,14 @@ class TemplateSelector extends Component
2223
2324 public ?string $ selectedTemplateId = null ;
2425
26+ /** Search query for filtering templates */
27+ public string $ search = '' ;
28+
29+ /** Preview slide-over state */
30+ public bool $ showPreview = false ;
31+
32+ public ?string $ previewTemplateId = null ;
33+
2534 public function mount (string $ projectId , string $ documentType ): void
2635 {
2736 $ this ->projectId = $ projectId ;
@@ -47,6 +56,119 @@ public function groupedTemplates(): SupportCollection
4756 return Template::getGroupedByCategory ($ this ->documentTypeEnum (), auth ()->id ());
4857 }
4958
59+ /** Get user's custom templates, filtered by search */
60+ #[Computed]
61+ public function myTemplates (): Collection
62+ {
63+ $ query = Template::query ()
64+ ->where ('user_id ' , auth ()->id ())
65+ ->where ('document_type ' , $ this ->documentTypeEnum ());
66+
67+ if ($ this ->search ) {
68+ $ query ->where (function ($ q ) {
69+ $ q ->where ('name ' , 'like ' , "% {$ this ->search }% " )
70+ ->orWhere ('description ' , 'like ' , "% {$ this ->search }% " );
71+ });
72+ }
73+
74+ return $ query ->orderBy ('name ' )->get ();
75+ }
76+
77+ /** Get built-in templates grouped by category, filtered by search */
78+ #[Computed]
79+ public function builtInGroupedTemplates (): SupportCollection
80+ {
81+ $ query = Template::query ()
82+ ->where ('document_type ' , $ this ->documentTypeEnum ())
83+ ->where (function ($ query ) {
84+ $ query ->where ('is_built_in ' , true )
85+ ->orWhere ('is_community ' , true )
86+ ->orWhere ('is_public ' , true );
87+ })
88+ ->whereNull ('user_id ' );
89+
90+ if ($ this ->search ) {
91+ $ query ->where (function ($ q ) {
92+ $ q ->where ('name ' , 'like ' , "% {$ this ->search }% " )
93+ ->orWhere ('description ' , 'like ' , "% {$ this ->search }% " );
94+ });
95+ }
96+
97+ return $ query
98+ ->orderBy ('sort_order ' )
99+ ->orderByDesc ('is_built_in ' )
100+ ->orderByDesc ('usage_count ' )
101+ ->get ()
102+ ->groupBy (fn (Template $ template ): string => $ template ->category ?->value ?? 'other ' )
103+ ->map (fn (Collection $ templates , string $ category ): array => [
104+ 'category ' => TemplateCategory::tryFrom ($ category ),
105+ 'templates ' => $ templates ,
106+ ]);
107+ }
108+
109+ /** Get the recommended template (PlanForge PRD or Tech Spec) */
110+ #[Computed]
111+ public function recommendedTemplate (): ?Template
112+ {
113+ $ recommendedName = $ this ->documentType === 'prd ' ? 'PlanForge PRD ' : 'PlanForge Tech Spec ' ;
114+
115+ return Template::query ()
116+ ->where ('name ' , $ recommendedName )
117+ ->where ('is_built_in ' , true )
118+ ->first ();
119+ }
120+
121+ /** Get quick option templates (No Template placeholder + alternative) */
122+ #[Computed]
123+ public function quickOptions (): array
124+ {
125+ // Find "No Template" option from core category
126+ $ noTemplate = Template::query ()
127+ ->where ('document_type ' , $ this ->documentTypeEnum ())
128+ ->where ('name ' , 'No Template ' )
129+ ->where ('is_built_in ' , true )
130+ ->first ();
131+
132+ // Find an alternative template (first non-recommended from core)
133+ $ recommended = $ this ->recommendedTemplate ;
134+ $ alternative = Template::query ()
135+ ->where ('document_type ' , $ this ->documentTypeEnum ())
136+ ->where ('category ' , TemplateCategory::Core)
137+ ->where ('is_built_in ' , true )
138+ ->when ($ recommended , fn ($ q ) => $ q ->where ('id ' , '!= ' , $ recommended ->id ))
139+ ->when ($ noTemplate , fn ($ q ) => $ q ->where ('id ' , '!= ' , $ noTemplate ->id ))
140+ ->orderBy ('sort_order ' )
141+ ->first ();
142+
143+ return array_filter ([$ noTemplate , $ alternative ]);
144+ }
145+
146+ /** Check if there are any search results */
147+ #[Computed]
148+ public function hasSearchResults (): bool
149+ {
150+ if (! $ this ->search ) {
151+ return true ;
152+ }
153+
154+ return $ this ->myTemplates ->isNotEmpty () || $ this ->builtInGroupedTemplates ->isNotEmpty ();
155+ }
156+
157+ /** Get total template count for display */
158+ #[Computed]
159+ public function totalTemplateCount (): int
160+ {
161+ return Template::query ()
162+ ->where ('document_type ' , $ this ->documentTypeEnum ())
163+ ->where (function ($ query ) {
164+ $ query ->where ('user_id ' , auth ()->id ())
165+ ->orWhere ('is_built_in ' , true )
166+ ->orWhere ('is_community ' , true )
167+ ->orWhere ('is_public ' , true );
168+ })
169+ ->count ();
170+ }
171+
50172 #[Computed]
51173 public function sectionCount (): int
52174 {
@@ -63,6 +185,17 @@ public function selectedTemplate(): ?Template
63185 return Template::find ($ this ->selectedTemplateId );
64186 }
65187
188+ /** Get template being previewed */
189+ #[Computed]
190+ public function previewTemplate (): ?Template
191+ {
192+ if (! $ this ->previewTemplateId ) {
193+ return null ;
194+ }
195+
196+ return Template::find ($ this ->previewTemplateId );
197+ }
198+
66199 public function selectTemplate (string $ templateId ): void
67200 {
68201 $ project = Project::findOrFail ($ this ->projectId );
@@ -71,7 +204,13 @@ public function selectTemplate(string $templateId): void
71204 $ this ->selectedTemplateId = $ templateId ;
72205 $ project ->update ([$ this ->templateField () => $ templateId ]);
73206
74- $ this ->dispatch ('template-selected ' , templateId: $ templateId );
207+ // Dispatch with template details for sticky bar
208+ $ template = Template::find ($ templateId );
209+ $ this ->dispatch ('template-selected ' ,
210+ templateId: $ templateId ,
211+ templateName: $ template ?->name ?? 'No Template ' ,
212+ sectionCount: count ($ template ?->sections ?? [])
213+ );
75214 }
76215
77216 public function clearTemplate (): void
@@ -85,6 +224,35 @@ public function clearTemplate(): void
85224 $ this ->dispatch ('template-cleared ' );
86225 }
87226
227+ /** Open preview slide-over for a template */
228+ public function preview (string $ templateId ): void
229+ {
230+ $ this ->previewTemplateId = $ templateId ;
231+ $ this ->showPreview = true ;
232+ }
233+
234+ /** Close preview slide-over */
235+ public function closePreview (): void
236+ {
237+ $ this ->showPreview = false ;
238+ $ this ->previewTemplateId = null ;
239+ }
240+
241+ /** Select template from preview and close slide-over */
242+ public function selectFromPreview (): void
243+ {
244+ if ($ this ->previewTemplateId ) {
245+ $ this ->selectTemplate ($ this ->previewTemplateId );
246+ $ this ->closePreview ();
247+ }
248+ }
249+
250+ /** Clear search query */
251+ public function clearSearch (): void
252+ {
253+ $ this ->search = '' ;
254+ }
255+
88256 public function render (): View
89257 {
90258 return view ('livewire.projects.partials.template-selector ' );
@@ -112,6 +280,21 @@ private function applyUserDefaultTemplate(Project $project): void
112280 if ($ defaultTemplateId ) {
113281 $ this ->selectedTemplateId = $ defaultTemplateId ;
114282 $ project ->update ([$ this ->templateField () => $ defaultTemplateId ]);
283+
284+ return ;
285+ }
286+
287+ // Fall back to recommended template if no user default
288+ $ this ->applyRecommendedTemplate ($ project );
289+ }
290+
291+ private function applyRecommendedTemplate (Project $ project ): void
292+ {
293+ $ recommended = $ this ->recommendedTemplate ;
294+
295+ if ($ recommended ) {
296+ $ this ->selectedTemplateId = $ recommended ->id ;
297+ $ project ->update ([$ this ->templateField () => $ recommended ->id ]);
115298 }
116299 }
117300}
0 commit comments