template improvements for scope choice and filtering
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
import type { Component } from 'solid-js';
|
||||
import { createSignal, Show, For, createEffect } from 'solid-js';
|
||||
import { createSignal, Show, For, createEffect, createMemo, type Component } from 'solid-js';
|
||||
import { Portal } from 'solid-js/web';
|
||||
import { X, FileBox, CheckCircle2 } from 'lucide-solid';
|
||||
import { X, FileBox, CheckCircle2, Search } from 'lucide-solid';
|
||||
import { pb, COLLECTIONS } from '../utils/db';
|
||||
import { appStore } from '../store/appStore';
|
||||
import type { EstimateItem, TemplateItem } from '../types';
|
||||
import type { EstimateItem, TemplateItem, Modifier } from '../types';
|
||||
|
||||
interface ApplyTemplateModalProps {
|
||||
show: boolean;
|
||||
@@ -17,9 +16,36 @@ const ApplyTemplateModal: Component<ApplyTemplateModalProps> = (props) => {
|
||||
|
||||
const [templates, setTemplates] = createSignal<any[]>([]);
|
||||
const [isLoading, setIsLoading] = createSignal(false);
|
||||
const [searchQuery, setSearchQuery] = createSignal('');
|
||||
const [targetSubScopeId, setTargetSubScopeId] = createSignal<string>('scope'); // 'scope' means no sub-scope selected
|
||||
const [isApplying, setIsApplying] = createSignal(false);
|
||||
|
||||
const filteredAndGroupedTemplates = createMemo(() => {
|
||||
const query = searchQuery().toLowerCase();
|
||||
let filtered = templates().filter(t =>
|
||||
t.name.toLowerCase().includes(query) ||
|
||||
(t.defaultSubScopeName || '').toLowerCase().includes(query)
|
||||
);
|
||||
|
||||
// Sort alphabetically by name
|
||||
filtered.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// Group by defaultSubScopeName
|
||||
const groups: Record<string, any[]> = {};
|
||||
filtered.forEach(t => {
|
||||
const groupName = t.defaultSubScopeName || 'Uncategorized';
|
||||
if (!groups[groupName]) groups[groupName] = [];
|
||||
groups[groupName].push(t);
|
||||
});
|
||||
|
||||
// Convert to array of groups for sorting groups
|
||||
return Object.entries(groups).sort(([a], [b]) => {
|
||||
if (a === 'Uncategorized') return 1;
|
||||
if (b === 'Uncategorized') return -1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (props.show) {
|
||||
loadTemplates();
|
||||
@@ -44,13 +70,21 @@ const ApplyTemplateModal: Component<ApplyTemplateModalProps> = (props) => {
|
||||
|
||||
const applyTemplate = (record: any) => {
|
||||
const templateItems: TemplateItem[] = record.items || [];
|
||||
if (templateItems.length === 0) {
|
||||
alert('This template has no items.');
|
||||
if (templateItems.length === 0 && (record.modifiers || []).length === 0) {
|
||||
alert('This template has no items or modifiers.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsApplying(true);
|
||||
|
||||
// Find default sub-scope if target is 'scope' and template has a defaultSubScopeName
|
||||
let finalSubScopeId = targetSubScopeId() !== 'scope' ? targetSubScopeId() : undefined;
|
||||
|
||||
if (!finalSubScopeId && record.defaultSubScopeName) {
|
||||
const match = subScopes().find(ss => ss.name.toLowerCase() === record.defaultSubScopeName.toLowerCase());
|
||||
if (match) finalSubScopeId = match.id;
|
||||
}
|
||||
|
||||
const newItems: EstimateItem[] = templateItems.map(item => ({
|
||||
id: crypto.randomUUID(),
|
||||
description: item.description,
|
||||
@@ -61,12 +95,27 @@ const ApplyTemplateModal: Component<ApplyTemplateModalProps> = (props) => {
|
||||
contingency: item.contingency,
|
||||
contingencyType: item.contingencyType,
|
||||
scopeId: props.scopeId || undefined,
|
||||
subScopeId: targetSubScopeId() !== 'scope' ? targetSubScopeId() : undefined
|
||||
subScopeId: finalSubScopeId
|
||||
}));
|
||||
|
||||
// Append to existing items
|
||||
appStore.setEstimateItems(prev => [...prev, ...newItems]);
|
||||
|
||||
// Copy modifiers if target is (or resolved to) a sub-scope
|
||||
if (finalSubScopeId) {
|
||||
const templateModifiers: Modifier[] = record.modifiers || [];
|
||||
if (templateModifiers.length > 0) {
|
||||
appStore.setSubScopes(
|
||||
ss => ss.id === finalSubScopeId,
|
||||
'modifiers',
|
||||
(existing = []) => [
|
||||
...existing,
|
||||
...templateModifiers.map(m => ({ ...m, id: crypto.randomUUID() }))
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setIsApplying(false);
|
||||
props.onClose();
|
||||
};
|
||||
@@ -92,50 +141,78 @@ const ApplyTemplateModal: Component<ApplyTemplateModalProps> = (props) => {
|
||||
</div>
|
||||
|
||||
<div class="flex-1 flex flex-col p-6 gap-6 overflow-hidden">
|
||||
{/* Target Selection */}
|
||||
<div class="space-y-3 shrink-0">
|
||||
<label class="text-xs font-black text-muted-foreground uppercase tracking-wider">Target Destination</label>
|
||||
<select
|
||||
value={targetSubScopeId()}
|
||||
onChange={(e) => setTargetSubScopeId(e.currentTarget.value)}
|
||||
class="w-full bg-muted/20 border border-border/60 p-3 rounded-2xl text-sm outline-none focus:bg-background focus:ring-4 focus:ring-primary/10 focus:border-primary transition-all font-medium appearance-none"
|
||||
>
|
||||
<option value="scope">Scope Level (Default)</option>
|
||||
<For each={subScopes()}>
|
||||
{(ss) => (
|
||||
<option value={ss.id}>Sub-scope: {ss.name}</option>
|
||||
)}
|
||||
</For>
|
||||
</select>
|
||||
{/* Target Selection & Search */}
|
||||
<div class="flex gap-4 shrink-0">
|
||||
<div class="flex-1 space-y-3">
|
||||
<label class="text-xs font-black text-muted-foreground uppercase tracking-wider">Target Destination</label>
|
||||
<select
|
||||
value={targetSubScopeId()}
|
||||
onChange={(e) => setTargetSubScopeId(e.currentTarget.value)}
|
||||
class="w-full bg-muted/20 border border-border/60 p-3 rounded-2xl text-sm outline-none focus:bg-background focus:ring-4 focus:ring-primary/10 focus:border-primary transition-all font-medium appearance-none"
|
||||
>
|
||||
<option value="scope">Scope Level (Default)</option>
|
||||
<For each={subScopes()}>
|
||||
{(ss) => (
|
||||
<option value={ss.id}>Sub-scope: {ss.name}</option>
|
||||
)}
|
||||
</For>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-1 space-y-3">
|
||||
<label class="text-xs font-black text-muted-foreground uppercase tracking-wider">Search Templates</label>
|
||||
<div class="relative">
|
||||
<Search class="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name or target..."
|
||||
value={searchQuery()}
|
||||
onInput={(e) => setSearchQuery(e.currentTarget.value)}
|
||||
class="w-full bg-muted/20 border border-border/60 pl-11 pr-4 py-3 rounded-2xl text-sm outline-none focus:bg-background focus:ring-4 focus:ring-primary/10 focus:border-primary transition-all font-medium"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Templates List */}
|
||||
<div class="flex-1 flex flex-col min-h-0 space-y-3">
|
||||
<label class="text-xs font-black text-muted-foreground uppercase tracking-wider">Select Template</label>
|
||||
<div class="flex-1 overflow-y-auto space-y-3 bg-muted/10 border border-border/50 rounded-2xl p-4">
|
||||
<div class="flex-1 overflow-y-auto space-y-6 bg-muted/10 border border-border/50 rounded-2xl p-4">
|
||||
<Show
|
||||
when={!isLoading()}
|
||||
fallback={<div class="py-12 text-center text-muted-foreground text-sm font-medium">Loading templates...</div>}
|
||||
>
|
||||
<For each={templates()}>
|
||||
{(record) => (
|
||||
<div class="group flex items-center justify-between p-4 rounded-xl border border-border/60 bg-card hover:border-primary/50 hover:shadow-premium transition-all">
|
||||
<div>
|
||||
<h4 class="font-bold text-foreground">{record.name}</h4>
|
||||
<p class="text-[10px] text-muted-foreground font-medium uppercase tracking-wider mt-1">
|
||||
{record.items?.length || 0} Items • {new Date(record.created).toLocaleDateString()}
|
||||
</p>
|
||||
<For each={filteredAndGroupedTemplates()}>
|
||||
{([groupName, groupTemplates]) => (
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center gap-2 px-1">
|
||||
<span class="text-[10px] font-black uppercase tracking-widest text-muted-foreground/60">{groupName}</span>
|
||||
<div class="flex-1 h-px bg-border/40"></div>
|
||||
</div>
|
||||
<div class="grid gap-3">
|
||||
<For each={groupTemplates}>
|
||||
{(record) => (
|
||||
<div class="group flex items-center justify-between p-4 rounded-xl border border-border/60 bg-card hover:border-primary/50 hover:shadow-premium transition-all">
|
||||
<div>
|
||||
<h4 class="font-bold text-foreground">{record.name}</h4>
|
||||
<p class="text-[10px] text-muted-foreground font-medium uppercase tracking-wider mt-1">
|
||||
{record.items?.length || 0} Items • {new Date(record.created).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
disabled={isApplying()}
|
||||
onClick={() => applyTemplate(record)}
|
||||
class="flex items-center gap-2 px-4 py-2 bg-primary/10 text-primary border border-primary/20 rounded-lg text-xs font-bold shadow-sm hover:bg-primary hover:text-primary-foreground hover:border-primary transition-all"
|
||||
>
|
||||
<CheckCircle2 class="w-4 h-4" /> Apply
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<button
|
||||
disabled={isApplying()}
|
||||
onClick={() => applyTemplate(record)}
|
||||
class="flex items-center gap-2 px-4 py-2 bg-primary/10 text-primary border border-primary/20 rounded-lg text-xs font-bold shadow-sm hover:bg-primary hover:text-primary-foreground hover:border-primary transition-all"
|
||||
>
|
||||
<CheckCircle2 class="w-4 h-4" /> Apply
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<Show when={templates().length === 0}>
|
||||
<div class="py-12 flex flex-col items-center justify-center text-center text-muted-foreground/60 border-2 border-dashed border-border/60 rounded-xl bg-background/50 h-full">
|
||||
<FileBox class="w-8 h-8 mb-3 opacity-50" />
|
||||
@@ -143,6 +220,12 @@ const ApplyTemplateModal: Component<ApplyTemplateModalProps> = (props) => {
|
||||
<p class="text-xs mt-1">Create one in the Template Creator first.</p>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={templates().length > 0 && filteredAndGroupedTemplates().length === 0}>
|
||||
<div class="py-12 text-center text-muted-foreground/50 italic text-sm">
|
||||
No templates match your search.
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -153,12 +153,20 @@ const ScopeCard: Component<ScopeCardProps> = (props) => {
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => props.onAddSubScope(props.scope.id)}
|
||||
class="flex items-center gap-2 px-4 py-2 bg-primary/10 text-primary rounded-xl text-sm font-bold hover:bg-primary/20 transition-colors border border-primary/20"
|
||||
>
|
||||
<Plus class="mr-2 h-3.5 w-3.5" /> Sub-scope
|
||||
</button>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<button
|
||||
onClick={() => props.onAddSubScope(props.scope.id)}
|
||||
class="flex items-center gap-2 px-3 py-2 bg-primary/10 text-primary rounded-xl text-sm font-bold hover:bg-primary/20 transition-colors border border-primary/20"
|
||||
>
|
||||
<Plus class="h-3.5 w-3.5" /> Sub-scope
|
||||
</button>
|
||||
<button
|
||||
onClick={() => props.onOpenApplyTemplate(props.scope.id)}
|
||||
class="flex items-center gap-2 px-3 py-2 bg-indigo-50 text-indigo-600 rounded-xl text-sm font-bold hover:bg-indigo-100 transition-colors border border-indigo-100"
|
||||
>
|
||||
<FileBox class="h-3.5 w-3.5" /> Template
|
||||
</button>
|
||||
</div>
|
||||
<div class="h-8 w-px bg-border mx-2"></div>
|
||||
<button
|
||||
onClick={() => setShowBulkActions(!showBulkActions())}
|
||||
@@ -207,14 +215,6 @@ const ScopeCard: Component<ScopeCardProps> = (props) => {
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center ml-auto">
|
||||
<button
|
||||
onClick={() => props.onOpenApplyTemplate(props.scope.id)}
|
||||
class="flex items-center gap-2 px-3 py-1.5 bg-background text-primary border border-primary/30 rounded-lg text-xs font-bold shadow-sm hover:bg-primary hover:text-primary-foreground hover:border-primary transition-all"
|
||||
>
|
||||
<FileBox class="w-3.5 h-3.5" /> Apply Template
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ const [generalPreNotes, setGeneralPreNotes] = createSignal('');
|
||||
// Template Creator State
|
||||
const [activeTemplateName, setActiveTemplateName] = createSignal('');
|
||||
const [activeTemplateItems, setActiveTemplateItems] = createStore<TemplateItem[]>([]);
|
||||
const [activeTemplateModifiers, setActiveTemplateModifiers] = createStore<Modifier[]>([]);
|
||||
const [activeTemplateSubScopeName, setActiveTemplateSubScopeName] = createSignal<string | undefined>(undefined);
|
||||
const [activeTemplateId, setActiveTemplateId] = createSignal<string | null>(null);
|
||||
|
||||
export const appStore = {
|
||||
@@ -111,6 +113,34 @@ export const appStore = {
|
||||
setTemplateName: setActiveTemplateName,
|
||||
templateItems: activeTemplateItems,
|
||||
setTemplateItems: setActiveTemplateItems,
|
||||
templateModifiers: activeTemplateModifiers,
|
||||
setTemplateModifiers: setActiveTemplateModifiers,
|
||||
templateSubScopeName: activeTemplateSubScopeName,
|
||||
setTemplateSubScopeName: setActiveTemplateSubScopeName,
|
||||
|
||||
addTemplateModifier(moduleId: string) {
|
||||
const module = ModifierRegistry.getModule(moduleId);
|
||||
const newModifier: Modifier = {
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
moduleId: module.id,
|
||||
name: module.defaultLabel,
|
||||
value: module.defaultValue,
|
||||
valueType: module.defaultValueType,
|
||||
unitLabel: module.defaultUnitLabel,
|
||||
type: module.id as any,
|
||||
includeInTotal: module.id !== 'man-hours'
|
||||
};
|
||||
setActiveTemplateModifiers(prev => [...prev, newModifier]);
|
||||
},
|
||||
updateTemplateModifier: (modifierId: string, updates: Partial<Modifier>) => {
|
||||
setActiveTemplateModifiers(
|
||||
(m: Modifier) => m.id === modifierId,
|
||||
updates
|
||||
);
|
||||
},
|
||||
removeTemplateModifier: (modifierId: string) => {
|
||||
setActiveTemplateModifiers(prev => prev.filter(mod => mod.id !== modifierId));
|
||||
},
|
||||
|
||||
resetEstimate: () => {
|
||||
setActiveItems([]);
|
||||
@@ -127,5 +157,10 @@ export const appStore = {
|
||||
setJobInfo({ number: '', name: '', address: '', workOrder: '' });
|
||||
setGeneralEstimateNotes('');
|
||||
setGeneralPreNotes('');
|
||||
setActiveTemplateName('');
|
||||
setActiveTemplateItems([]);
|
||||
setActiveTemplateModifiers([]);
|
||||
setActiveTemplateSubScopeName(undefined);
|
||||
setActiveTemplateId(null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -68,4 +68,6 @@ export interface Template {
|
||||
id: string;
|
||||
name: string;
|
||||
items: TemplateItem[];
|
||||
modifiers?: Modifier[];
|
||||
defaultSubScopeName?: string;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { Component } from 'solid-js';
|
||||
import { FolderOpen, Save } from 'lucide-solid';
|
||||
import { For, createSignal, createMemo, type Component } from 'solid-js';
|
||||
import { FolderOpen, Save, Calculator, Plus } from 'lucide-solid';
|
||||
import type { TemplateItem } from '../types';
|
||||
import TemplateItemList from '../components/template/TemplateItemList';
|
||||
import ModifierRow from '../components/ModifierRow';
|
||||
import { ModifierRegistry, MODIFIER_MODULES } from '../utils/modifier-registry';
|
||||
import TemplateLoadModal from '../components/template/TemplateLoadModal';
|
||||
import { pb, COLLECTIONS } from '../utils/db';
|
||||
import { createSignal } from 'solid-js';
|
||||
|
||||
import { appStore } from '../store/appStore';
|
||||
|
||||
@@ -15,6 +16,14 @@ const TemplateCreator: Component = () => {
|
||||
const setTemplateName = appStore.setTemplateName;
|
||||
const items = appStore.templateItems;
|
||||
const setItems = appStore.setTemplateItems;
|
||||
const modifiers = appStore.templateModifiers;
|
||||
const setModifiers = appStore.setTemplateModifiers;
|
||||
const defaultSubScopeName = appStore.templateSubScopeName;
|
||||
const setDefaultSubScopeName = appStore.setTemplateSubScopeName;
|
||||
|
||||
const templateSubtotal = createMemo(() => {
|
||||
return items.reduce((sum, item) => sum + (item.quantity * item.unitPrice), 0);
|
||||
});
|
||||
|
||||
const [showLoadModal, setShowLoadModal] = createSignal(false);
|
||||
|
||||
@@ -44,21 +53,27 @@ const TemplateCreator: Component = () => {
|
||||
setTemplateId(record.id);
|
||||
setTemplateName(record.name);
|
||||
setItems(JSON.parse(JSON.stringify(record.items || [])));
|
||||
setModifiers(JSON.parse(JSON.stringify(record.modifiers || [])));
|
||||
setDefaultSubScopeName(record.defaultSubScopeName);
|
||||
};
|
||||
|
||||
const handleNewTemplate = () => {
|
||||
setTemplateId(null);
|
||||
setTemplateName('');
|
||||
setItems([]);
|
||||
setModifiers([]);
|
||||
setDefaultSubScopeName(undefined);
|
||||
};
|
||||
|
||||
const handleSaveTemplate = async () => {
|
||||
if (!templateName() || items.length === 0) return;
|
||||
if (!templateName() || (items.length === 0 && modifiers.length === 0)) return;
|
||||
|
||||
try {
|
||||
const record = {
|
||||
name: templateName(),
|
||||
items: JSON.parse(JSON.stringify(items)) // Ensure plain objects for PocketBase
|
||||
items: JSON.parse(JSON.stringify(items)), // Ensure plain objects for PocketBase
|
||||
modifiers: JSON.parse(JSON.stringify(modifiers)),
|
||||
defaultSubScopeName: defaultSubScopeName()
|
||||
};
|
||||
|
||||
if (templateId()) {
|
||||
@@ -100,7 +115,7 @@ const TemplateCreator: Component = () => {
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSaveTemplate}
|
||||
disabled={!templateName() || items.length === 0}
|
||||
disabled={!templateName() || (items.length === 0 && modifiers.length === 0)}
|
||||
class="flex items-center gap-2 px-6 py-2.5 text-sm font-bold text-white bg-blue-600 rounded-xl hover:bg-blue-700 transition-all shadow-premium hover:shadow-premium-hover disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Save class="w-4 h-4" />
|
||||
@@ -109,17 +124,34 @@ const TemplateCreator: Component = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-8">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Sub-Scope Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={templateName()}
|
||||
onInput={(e) => setTemplateName(e.currentTarget.value)}
|
||||
placeholder="e.g. Master Bathroom Rough-in"
|
||||
class="w-full bg-gray-50/50 border border-gray-200 rounded-xl px-4 py-3 text-gray-800 focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 outline-none font-medium transition-all shadow-sm"
|
||||
/>
|
||||
<div class="flex gap-6 mb-8">
|
||||
<div class="flex-1">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Sub-Scope Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={templateName()}
|
||||
onInput={(e) => setTemplateName(e.currentTarget.value)}
|
||||
placeholder="e.g. Master Bathroom Rough-in"
|
||||
class="w-full bg-gray-50/50 border border-gray-200 rounded-xl px-4 py-3 text-gray-800 focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 outline-none font-medium transition-all shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-64">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Default Target
|
||||
</label>
|
||||
<select
|
||||
value={defaultSubScopeName() || ''}
|
||||
onChange={(e) => setDefaultSubScopeName(e.currentTarget.value || undefined)}
|
||||
class="w-full bg-gray-50/50 border border-gray-200 rounded-xl px-4 py-3 text-gray-800 focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 outline-none font-medium transition-all shadow-sm appearance-none"
|
||||
>
|
||||
<option value="">None (Scope Level)</option>
|
||||
<option value="Materials">Materials</option>
|
||||
<option value="Labor">Labor</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TemplateItemList
|
||||
@@ -128,6 +160,54 @@ const TemplateCreator: Component = () => {
|
||||
onUpdateItem={handleUpdateItem}
|
||||
onDeleteItem={handleDeleteItem}
|
||||
/>
|
||||
|
||||
{/* Modifiers Section */}
|
||||
<div class="mt-12 pt-8 border-t border-gray-100">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 bg-blue-50 text-blue-600 rounded-xl">
|
||||
<Calculator class="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-lg text-gray-800">Template Modifiers</h3>
|
||||
<p class="text-xs text-gray-500 font-medium">Standard fees or adjustments for this sub-scope</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<For each={MODIFIER_MODULES}>
|
||||
{(module) => (
|
||||
<button
|
||||
onClick={() => appStore.addTemplateModifier(module.id)}
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 bg-white border border-gray-200 rounded-xl text-[10px] font-black uppercase tracking-wider text-gray-500 hover:text-blue-600 hover:border-blue-300 transition-all group shadow-sm"
|
||||
>
|
||||
<Plus class="w-3.5 h-3.5 transition-transform group-hover:rotate-90" />
|
||||
{module.name}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 bg-gray-50/50 rounded-2xl p-6 border border-gray-100">
|
||||
<For each={modifiers}>
|
||||
{(modifier) => (
|
||||
<ModifierRow
|
||||
modifier={modifier}
|
||||
calculatedValue={ModifierRegistry.calculateDisplay(modifier, templateSubtotal(), [])}
|
||||
onUpdate={appStore.updateTemplateModifier}
|
||||
onDelete={appStore.removeTemplateModifier}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
|
||||
{modifiers.length === 0 && (
|
||||
<div class="text-center py-8 text-gray-400 text-sm font-medium italic">
|
||||
No modifiers added to this template.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TemplateLoadModal
|
||||
|
||||
Reference in New Issue
Block a user