Tables Good
This commit is contained in:
@@ -23,7 +23,6 @@ const ItemExtractor: Component<ItemExtractorProps> = () => {
|
||||
const ignoredLines = appStore.ignoredLines;
|
||||
const setIgnoredLines = appStore.setIgnoredLines;
|
||||
const estimateItems = appStore.estimateItems;
|
||||
const setEstimateName = appStore.setEstimateName;
|
||||
|
||||
const [showToast] = createSignal(false);
|
||||
const [toastMessage] = createSignal('');
|
||||
@@ -80,11 +79,6 @@ const ItemExtractor: Component<ItemExtractorProps> = () => {
|
||||
<div class="w-full">
|
||||
<EstimateBuilder
|
||||
initialItems={items()}
|
||||
onReset={() => {
|
||||
setItems([]);
|
||||
setIgnoredLines([]);
|
||||
setEstimateName('');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -15,6 +15,7 @@ import CloudLoadModal from './CloudLoadModal';
|
||||
import ExportTableModal from './ExportTableModal';
|
||||
import ApplyTemplateModal from './ApplyTemplateModal';
|
||||
import HistoryModal from './estimate-builder/HistoryModal';
|
||||
import { NewEstimateModal } from './estimate-builder/NewEstimateModal';
|
||||
|
||||
// Import extracted components & hooks
|
||||
import { useEstimateCalculations } from './estimate-builder/useEstimateCalculations';
|
||||
@@ -33,7 +34,6 @@ import { ScopesList } from './estimate-builder/ScopesList';
|
||||
|
||||
interface EstimateBuilderProps {
|
||||
initialItems: ExtractedItem[];
|
||||
onReset: () => void;
|
||||
}
|
||||
|
||||
const EstimateBuilder: Component<EstimateBuilderProps> = (props) => {
|
||||
@@ -48,6 +48,7 @@ const EstimateBuilder: Component<EstimateBuilderProps> = (props) => {
|
||||
const [showSaveModal, setShowSaveModal] = createSignal(false);
|
||||
const [showLoadModal, setShowLoadModal] = createSignal(false);
|
||||
const [showHistoryModal, setShowHistoryModal] = createSignal(false);
|
||||
const [showNewEstimateModal, setShowNewEstimateModal] = createSignal(false);
|
||||
const [showExportTableModal, setShowExportTableModal] = createSignal(false);
|
||||
const [applyTemplateScopeId, setApplyTemplateScopeId] = createSignal<string | null>(null);
|
||||
|
||||
@@ -105,6 +106,7 @@ const EstimateBuilder: Component<EstimateBuilderProps> = (props) => {
|
||||
setScopes(prev => [...prev, newScope]);
|
||||
addSubScopeWithName(scopeId, 'Materials');
|
||||
addSubScopeWithName(scopeId, 'Labor');
|
||||
return scopeId;
|
||||
};
|
||||
|
||||
const addSubScopeWithName = (scopeId: string, name: string) => {
|
||||
@@ -114,6 +116,7 @@ const EstimateBuilder: Component<EstimateBuilderProps> = (props) => {
|
||||
scopeId
|
||||
};
|
||||
setSubScopes(prev => [...prev, newSubScope]);
|
||||
return newSubScope.id;
|
||||
};
|
||||
|
||||
const addSubScope = (scopeId: string) => {
|
||||
@@ -195,14 +198,14 @@ const EstimateBuilder: Component<EstimateBuilderProps> = (props) => {
|
||||
isSidebarCollapsed={isSidebarCollapsed()}
|
||||
onToggleSidebar={() => setIsSidebarCollapsed(!isSidebarCollapsed())}
|
||||
onAddScope={addScope}
|
||||
onReset={props.onReset}
|
||||
onShowNewEstimateModal={() => setShowNewEstimateModal(true)}
|
||||
onImportEstimate={importEstimate}
|
||||
onShowExportTableModal={() => setShowExportTableModal(true)}
|
||||
onExportEstimate={exportEstimate}
|
||||
onShowLoadModal={() => setShowLoadModal(true)}
|
||||
onShowSaveModal={() => setShowSaveModal(true)}
|
||||
onShowHistoryModal={() => setShowHistoryModal(true)}
|
||||
onSaveChanges={saveChangesToCloud}
|
||||
onSaveChanges={() => appStore.isLatestVersion() ? saveChangesToCloud() : saveAsNewestToCloud()}
|
||||
/>
|
||||
|
||||
<div class="flex flex-1 relative items-start">
|
||||
@@ -358,6 +361,29 @@ const EstimateBuilder: Component<EstimateBuilderProps> = (props) => {
|
||||
<ExportTableModal show={showExportTableModal()} onClose={() => setShowExportTableModal(false)} />
|
||||
<ApplyTemplateModal show={!!applyTemplateScopeId()} scopeId={applyTemplateScopeId()} onClose={() => setApplyTemplateScopeId(null)} />
|
||||
<HistoryModal show={showHistoryModal()} onClose={() => setShowHistoryModal(false)} onCheckout={checkoutVersion} />
|
||||
|
||||
<NewEstimateModal
|
||||
show={showNewEstimateModal()}
|
||||
onClose={() => setShowNewEstimateModal(false)}
|
||||
onResetCSV={() => {
|
||||
appStore.resetEstimate();
|
||||
setShowNewEstimateModal(false);
|
||||
}}
|
||||
onManualEstimate={() => {
|
||||
appStore.resetEstimate();
|
||||
setItems([{
|
||||
id: crypto.randomUUID(),
|
||||
description: 'Item',
|
||||
quantity: 1,
|
||||
unitPrice: 0,
|
||||
markup: 0,
|
||||
markupType: 'percent',
|
||||
contingency: 0,
|
||||
contingencyType: 'percent'
|
||||
}]);
|
||||
setShowNewEstimateModal(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Show } from 'solid-js';
|
||||
import { createSignal, onCleanup, Show } from 'solid-js';
|
||||
import type { Component } from 'solid-js';
|
||||
import { PanelLeftOpen, PanelLeftClose, Calculator, Plus, FileUp, Upload, Table, Download, History, Save } from 'lucide-solid';
|
||||
import { PanelLeftOpen, PanelLeftClose, Calculator, Plus, Upload, Table, Download, History, Save, ChevronDown, MoreHorizontal } from 'lucide-solid';
|
||||
import { appStore } from '../../store/appStore';
|
||||
|
||||
interface BuilderHeaderProps {
|
||||
isSidebarCollapsed: boolean;
|
||||
onToggleSidebar: () => void;
|
||||
onAddScope: () => void;
|
||||
onReset: () => void;
|
||||
onShowNewEstimateModal: () => void;
|
||||
onImportEstimate: (e: Event) => void;
|
||||
onShowExportTableModal: () => void;
|
||||
onExportEstimate: () => void;
|
||||
@@ -19,6 +19,17 @@ interface BuilderHeaderProps {
|
||||
|
||||
export const BuilderHeader: Component<BuilderHeaderProps> = (props) => {
|
||||
const { scopes, estimateItems: items } = appStore;
|
||||
const [showActions, setShowActions] = createSignal(false);
|
||||
let dropdownRef: HTMLDivElement | undefined;
|
||||
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (dropdownRef && !dropdownRef.contains(e.target as Node)) {
|
||||
setShowActions(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('click', handleClickOutside);
|
||||
onCleanup(() => window.removeEventListener('click', handleClickOutside));
|
||||
|
||||
return (
|
||||
<div class="min-h-[4rem] py-3 px-6 border-b border-border bg-background/50 flex flex-wrap gap-y-4 items-center justify-between sticky top-0 z-50 backdrop-blur-md">
|
||||
@@ -46,38 +57,53 @@ export const BuilderHeader: Component<BuilderHeaderProps> = (props) => {
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
onClick={props.onAddScope}
|
||||
class="flex items-center gap-2 px-4 py-2 bg-foreground text-background rounded-xl text-sm font-bold hover:bg-foreground/90 transition-all shadow-lg shadow-foreground/10 mr-2"
|
||||
onClick={props.onShowNewEstimateModal}
|
||||
class="flex items-center gap-2 px-3 py-2 bg-muted/50 text-blue-600 rounded-xl text-xs font-black hover:bg-blue-100/50 transition-all border border-blue-200"
|
||||
>
|
||||
<Plus class="w-4 h-4" /> New Scope
|
||||
</button>
|
||||
<button
|
||||
onClick={props.onReset}
|
||||
class="flex items-center gap-2 px-3 py-2 bg-muted/50 text-primary rounded-xl text-xs font-black hover:bg-primary/10 transition-all border border-border"
|
||||
>
|
||||
<FileUp class="w-4 h-4" /> New CSV
|
||||
</button>
|
||||
<div class="h-6 w-px bg-border mx-1"></div>
|
||||
<label class="flex items-center gap-2 px-3 py-2 bg-muted text-muted-foreground rounded-xl text-xs font-bold hover:bg-accent hover:text-accent-foreground transition-all border border-border cursor-pointer">
|
||||
<Upload class="w-4 h-4" /> Import JSON
|
||||
<input type="file" accept=".json" class="hidden" onChange={props.onImportEstimate} />
|
||||
</label>
|
||||
<button
|
||||
onClick={props.onShowExportTableModal}
|
||||
class="flex items-center gap-2 px-3 py-2 bg-muted text-muted-foreground rounded-xl text-xs font-bold hover:bg-accent hover:text-accent-foreground transition-all border border-border mr-2"
|
||||
>
|
||||
<Table class="w-4 h-4" /> Export Table
|
||||
</button>
|
||||
<button
|
||||
onClick={props.onExportEstimate}
|
||||
class="flex items-center gap-2 px-3 py-2 bg-muted text-muted-foreground rounded-xl text-xs font-bold hover:bg-accent hover:text-accent-foreground transition-all border border-border mr-2"
|
||||
>
|
||||
<Download class="w-4 h-4" /> Export JSON
|
||||
<Plus class="w-4 h-4" /> New Estimate
|
||||
</button>
|
||||
|
||||
<div class="relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setShowActions(!showActions()); }}
|
||||
class="flex items-center gap-2 px-3 py-2 bg-muted text-muted-foreground rounded-xl text-xs font-bold hover:bg-accent hover:text-accent-foreground transition-all border border-border"
|
||||
>
|
||||
<MoreHorizontal class="w-4 h-4" /> Actions <ChevronDown class={`w-3 h-3 transition-transform ${showActions() ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
|
||||
<Show when={showActions()}>
|
||||
<div class="absolute right-0 mt-2 w-56 bg-card border border-border rounded-2xl shadow-premium z-[60] overflow-hidden animate-in fade-in zoom-in-95 duration-150 origin-top-right">
|
||||
<div class="p-2 space-y-1">
|
||||
<p class="text-[9px] font-black uppercase tracking-widest text-muted-foreground/60 px-3 py-2">Data Exchange</p>
|
||||
<label class="flex items-center gap-3 px-3 py-2.5 text-xs font-bold text-foreground hover:bg-muted rounded-xl cursor-pointer transition-colors w-full">
|
||||
<Upload class="w-4 h-4 text-blue-500" /> Import JSON
|
||||
<input type="file" accept=".json" class="hidden" onChange={(e) => { props.onImportEstimate(e); setShowActions(false); }} />
|
||||
</label>
|
||||
<button
|
||||
onClick={() => { props.onExportEstimate(); setShowActions(false); }}
|
||||
class="flex items-center gap-3 px-3 py-2.5 text-xs font-bold text-foreground hover:bg-muted rounded-xl transition-colors w-full text-left"
|
||||
>
|
||||
<Download class="w-4 h-4 text-blue-500" /> Export JSON
|
||||
</button>
|
||||
</div>
|
||||
<div class="border-t border-border/50 p-2 space-y-1 bg-muted/10">
|
||||
<p class="text-[9px] font-black uppercase tracking-widest text-muted-foreground/60 px-3 py-2">Summaries</p>
|
||||
<button
|
||||
onClick={() => { props.onShowExportTableModal(); setShowActions(false); }}
|
||||
class="flex items-center gap-3 px-3 py-2.5 text-xs font-bold text-foreground hover:bg-muted rounded-xl transition-colors w-full text-left"
|
||||
>
|
||||
<Table class="w-4 h-4 text-purple-500" /> Export Table
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="h-6 w-px bg-border mx-1"></div>
|
||||
|
||||
<button
|
||||
onClick={props.onShowLoadModal}
|
||||
class="flex items-center gap-2 px-3 py-2 bg-blue-50 text-blue-600 rounded-xl text-xs font-bold hover:bg-blue-100 transition-all border border-blue-200"
|
||||
class="flex items-center gap-2 px-4 py-2 bg-blue-50 text-blue-600 rounded-xl text-xs font-bold hover:bg-blue-100 transition-all border border-blue-200"
|
||||
>
|
||||
<Download class="w-4 h-4" /> Load
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Portal } from 'solid-js/web';
|
||||
import { Show } from 'solid-js';
|
||||
import { X, FileSpreadsheet, PlusCircle, AlertCircle } from 'lucide-solid';
|
||||
|
||||
interface NewEstimateModalProps {
|
||||
show: boolean;
|
||||
onClose: () => void;
|
||||
onResetCSV: () => void;
|
||||
onManualEstimate: () => void;
|
||||
}
|
||||
|
||||
export const NewEstimateModal = (props: NewEstimateModalProps) => {
|
||||
return (
|
||||
<Show when={props.show}>
|
||||
<Portal>
|
||||
<div class="fixed inset-0 z-[100] flex items-center justify-center bg-background/80 backdrop-blur-sm p-4 animate-in fade-in duration-200">
|
||||
<div class="bg-card w-full max-w-lg rounded-3xl shadow-premium border border-border overflow-hidden animate-in zoom-in-95 duration-200">
|
||||
<div class="p-6 border-b border-border/50 flex items-center justify-between bg-muted/30">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 bg-primary/10 rounded-xl text-primary">
|
||||
<PlusCircle class="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-black text-foreground">New Estimate</h3>
|
||||
<p class="text-[10px] text-muted-foreground font-black uppercase tracking-widest mt-0.5">Start a fresh project</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={props.onClose}
|
||||
class="p-2 hover:bg-background rounded-xl transition-all group"
|
||||
>
|
||||
<X class="w-5 h-5 text-muted-foreground group-hover:text-foreground transition-colors" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-8 space-y-6">
|
||||
<div class="p-4 bg-amber-50 border border-amber-200 rounded-2xl flex gap-3">
|
||||
<AlertCircle class="w-5 h-5 text-amber-600 shrink-0" />
|
||||
<div class="text-xs text-amber-900 font-medium">
|
||||
Starting a new estimate will clear all unsaved progress in the current project. Make sure you've saved any changes to the cloud first.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<button
|
||||
onClick={props.onResetCSV}
|
||||
class="flex flex-col items-center text-center p-6 bg-muted/30 border border-border/60 rounded-3xl hover:border-primary/50 hover:bg-primary/5 transition-all group gap-4"
|
||||
>
|
||||
<div class="p-4 bg-background rounded-2xl shadow-sm group-hover:shadow-md transition-all group-hover:scale-110">
|
||||
<FileSpreadsheet class="w-8 h-8 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-black text-foreground mb-1 text-sm">New from CSV</h4>
|
||||
<p class="text-[10px] text-muted-foreground font-medium leading-relaxed">
|
||||
Clear current state and upload a new estimate CSV file to begin.
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={props.onManualEstimate}
|
||||
class="flex flex-col items-center text-center p-6 bg-muted/30 border border-border/60 rounded-3xl hover:border-primary/50 hover:bg-primary/5 transition-all group gap-4"
|
||||
>
|
||||
<div class="p-4 bg-background rounded-2xl shadow-sm group-hover:shadow-md transition-all group-hover:scale-110">
|
||||
<PlusCircle class="w-8 h-8 text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-black text-foreground mb-1 text-sm">Manual Estimate</h4>
|
||||
<p class="text-[10px] text-muted-foreground font-medium leading-relaxed">
|
||||
Start with a blank estimate and a single default item.
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6 bg-muted/30 border-t border-border/50 flex justify-end">
|
||||
<button
|
||||
onClick={props.onClose}
|
||||
class="px-6 py-2.5 rounded-xl font-bold text-sm hover:bg-background border border-transparent hover:border-border transition-all"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
+18
-1
@@ -75,5 +75,22 @@ export const appStore = {
|
||||
templateName: activeTemplateName,
|
||||
setTemplateName: setActiveTemplateName,
|
||||
templateItems: activeTemplateItems,
|
||||
setTemplateItems: setActiveTemplateItems
|
||||
setTemplateItems: setActiveTemplateItems,
|
||||
|
||||
resetEstimate: () => {
|
||||
setActiveItems([]);
|
||||
setActiveIgnoredLines([]);
|
||||
setActiveEstimateId(null);
|
||||
setActiveEstimateName('');
|
||||
setActiveHistory([]);
|
||||
setActiveLatestSnapshot(null);
|
||||
setIsLatestVersion(true);
|
||||
setScopes([]);
|
||||
setSubScopes([]);
|
||||
setEstimateItems([]);
|
||||
setClientInfo({ name: '', address: '', phone: '', fax: '' });
|
||||
setJobInfo({ number: '', name: '', address: '', workOrder: '' });
|
||||
setGeneralEstimateNotes('');
|
||||
setGeneralPreNotes('');
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user