From f9a19eb4c4c847e3198ba409fe87c7151a9a3066 Mon Sep 17 00:00:00 2001 From: Timothy Cardoza Date: Wed, 18 Mar 2026 18:40:56 -0500 Subject: [PATCH] Auth added and UI improvements --- src/AppContainer.tsx | 22 ++- src/ItemExtractor.tsx | 128 ++++++++++++++---- src/components/CloudLoadModal.tsx | 8 +- .../estimate-builder/HistoryModal.tsx | 24 +++- .../estimate-builder/useEstimateSync.ts | 37 ++++- src/index.tsx | 27 +++- src/store/authStore.ts | 22 +++ src/views/Login.tsx | 87 ++++++++++++ 8 files changed, 317 insertions(+), 38 deletions(-) create mode 100644 src/store/authStore.ts create mode 100644 src/views/Login.tsx diff --git a/src/AppContainer.tsx b/src/AppContainer.tsx index 930f3d8..9c0b4ea 100644 --- a/src/AppContainer.tsx +++ b/src/AppContainer.tsx @@ -1,14 +1,25 @@ import type { Component } from 'solid-js'; -import { A } from '@solidjs/router'; +import { A, useLocation, useNavigate } from '@solidjs/router'; +import { Show } from 'solid-js'; +import { authStore } from './store/authStore'; interface AppContainerProps { children?: any; } const AppContainer: Component = (props) => { + const location = useLocation(); + const navigate = useNavigate(); + + const handleLogout = () => { + authStore.logout(); + navigate('/login'); + }; + return (
{/* Top Navigation Bar */} +
@@ -41,9 +52,18 @@ const AppContainer: Component = (props) => {
+
+ +
+ {/* Main Content Area */}
diff --git a/src/ItemExtractor.tsx b/src/ItemExtractor.tsx index 1d85f40..da4a797 100644 --- a/src/ItemExtractor.tsx +++ b/src/ItemExtractor.tsx @@ -7,7 +7,7 @@ import IgnoredLinesList from './components/IgnoredLinesList'; import Toast from './components/Toast'; import { extractItemsFromCSV } from './utils/csv-parser'; import type { ExtractedItem } from './utils/csv-parser'; -import { FileSpreadsheet, Download } from 'lucide-solid'; +import { FileSpreadsheet, Download, Plus, FilePlus, ArrowLeft } from 'lucide-solid'; import { appStore } from './store/appStore'; import CloudLoadModal from './components/CloudLoadModal'; @@ -23,10 +23,12 @@ const ItemExtractor: Component = () => { const ignoredLines = appStore.ignoredLines; const setIgnoredLines = appStore.setIgnoredLines; const estimateItems = appStore.estimateItems; + const scopes = appStore.scopes; const [showToast] = createSignal(false); const [toastMessage] = createSignal(''); const [showLoadModal, setShowLoadModal] = createSignal(false); + const [mode, setMode] = createSignal<'initial' | 'new-selection'>('initial'); const handleFileSelect = (csvText: string) => { const { items: extracted, ignored } = extractItemsFromCSV(csvText); @@ -34,42 +36,116 @@ const ItemExtractor: Component = () => { setIgnoredLines(ignored); }; + const handleNewBlankEstimate = () => { + appStore.resetEstimate(); + // Add a default empty scope to trigger the builder view + appStore.setScopes([{ + id: Math.random().toString(36).substr(2, 9), + name: 'Main Scope', + managementFee: 0, + managementFeeType: 'percent', + managementFeeQuantity: 0, + managementFeeRate: 0, + deliveryFee: 0, + deliveryFeeType: 'percent', + deliveryFeeQuantity: 0, + deliveryFeeRate: 0, + useManagementLogic: 'simple', + useDeliveryLogic: 'simple' + }]); + }; + return (
{/* Header - Hidden after upload or load */} - +
-
-
-

Item Parser & Extractor

-

Upload your CSV to extract "Item" fields and split quantities.

-
-
- -
-
- - -
-
-
- OR -
+ +
+
+

EstiMaker

+

Start a new project or continue where you left off.

+
+ +
+ + + +
- -
+ + + +
+ + +
+
+

Create New Estimate

+

Choose how you want to start your estimate.

+
+
+ +
+
+
+ +

Item Extractor

+
+

Upload a CSV file to automatically extract items and quantities.

+ +
+ + +
+
+
{/* Results */} 0 || estimateItems.length > 0} + when={items().length > 0 || estimateItems.length > 0 || scopes.length > 0} fallback={

Results will appear here

diff --git a/src/components/CloudLoadModal.tsx b/src/components/CloudLoadModal.tsx index 9f05c18..52d665d 100644 --- a/src/components/CloudLoadModal.tsx +++ b/src/components/CloudLoadModal.tsx @@ -1,7 +1,7 @@ import type { Component } from 'solid-js'; import { createSignal, Show, For, createEffect } from 'solid-js'; import { Portal } from 'solid-js/web'; -import { X } from 'lucide-solid'; +import { X, User } from 'lucide-solid'; import { pb, COLLECTIONS } from '../utils/db'; import { appStore } from '../store/appStore'; import { normalizeScopes } from '../utils/scope-utils'; @@ -85,6 +85,12 @@ const CloudLoadModal: Component = (props) => {

{new Date(record.created).toLocaleDateString()}

+ +
+ + {record.items.savedBy} +
+
@@ -102,6 +108,11 @@ const HistoryModal = (props: HistoryModalProps) => { ↳ {sum.fieldChanges} field adjustments

+ +

+ ↳ Scope Description/Notes updated +

+
)} @@ -128,6 +139,12 @@ const HistoryModal = (props: HistoryModalProps) => {

{new Date(snapshot.timestamp || Date.now()).toLocaleString()}

+ +
+ + Saved by {snapshot.savedBy} +
+
@@ -169,6 +186,11 @@ const HistoryModal = (props: HistoryModalProps) => { ↳ {sum.fieldChanges} field adjustments

+ +

+ ↳ Scope Description/Notes updated +

+
)} diff --git a/src/components/estimate-builder/useEstimateSync.ts b/src/components/estimate-builder/useEstimateSync.ts index 1f64b04..06b78c4 100644 --- a/src/components/estimate-builder/useEstimateSync.ts +++ b/src/components/estimate-builder/useEstimateSync.ts @@ -74,13 +74,14 @@ export function useEstimateSync( jobInfo: { ...jobInfo }, generalEstimateNotes: generalEstimateNotes(), generalPreNotes: generalPreNotes(), - timestamp: new Date().toISOString() + timestamp: new Date().toISOString(), + savedBy: pb.authStore.model?.name || pb.authStore.model?.email || 'Unknown' }; }; const calculateChangeSummary = (prevData: any, newData: any) => { - const summary: Record = {}; + const summary: Record = {}; const prevItems = prevData.items || []; const newItems = newData.items || []; @@ -104,6 +105,19 @@ export function useEstimateSync( let removed = 0; let updated = 0; let fields = 0; + let scopeMetadataUpdated = false; + + // Check Scope Metadata Changes + const prevScope = (prevData.scopes || []).find((s: any) => s.id === scopeId); + const currentScope = (newData.scopes || []).find((s: any) => s.id === scopeId); + if (prevScope && currentScope) { + ['bidDescription', 'estimatorNotes', 'name', 'managementFee', 'deliveryFee', 'useManagementLogic', 'useDeliveryLogic'].forEach(k => { + if (String(currentScope[k]) !== String(prevScope[k])) { + fields++; + scopeMetadataUpdated = true; + } + }); + } const scopePrevMap = new Map(scopePrevItems.map((i: any) => [i.id, i])); const scopeNewMap = new Map(scopeNewItems.map((i: any) => [i.id, i])); @@ -114,7 +128,7 @@ export function useEstimateSync( added++; } else { let changed = false; - ['description', 'quantity', 'unit', 'price', 'type'].forEach(k => { + ['description', 'quantity', 'unitPrice', 'markup', 'markupType', 'contingency', 'contingencyType'].forEach(k => { const v1 = (item as any)[k]; const v2 = (prev as any)[k]; if (String(v1) !== String(v2)) { @@ -132,8 +146,15 @@ export function useEstimateSync( } }); - if (added > 0 || removed > 0 || updated > 0 || fields > 0) { - (summary as any)[scopeId] = { scopeName, itemsAdded: added, itemsRemoved: removed, itemsUpdated: updated, fieldChanges: fields }; + if (added > 0 || removed > 0 || updated > 0 || fields > 0 || scopeMetadataUpdated) { + summary[scopeId] = { + scopeName: String(scopeName), + itemsAdded: added, + itemsRemoved: removed, + itemsUpdated: updated, + fieldChanges: fields, + scopeMetadataUpdated + }; } }); @@ -147,7 +168,8 @@ export function useEstimateSync( const record = await pb.collection(COLLECTIONS.ESTIMATES).create({ name, items: { ...data, changeSummary: null }, - history: [] + history: [], + userId: pb.authStore.model?.id }); setShowSaveModal(false); @@ -184,7 +206,8 @@ export function useEstimateSync( await pb.collection(COLLECTIONS.ESTIMATES).update(id, { items: newTip, - history: [...updatedHistory, record.items] + history: [...updatedHistory, record.items], + userId: pb.authStore.model?.id }); setHistory([...updatedHistory, record.items]); diff --git a/src/index.tsx b/src/index.tsx index 648793a..d0c9c27 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,10 +1,13 @@ import { render } from 'solid-js/web'; -import { Router, Route } from '@solidjs/router'; +import { Router, Route, useNavigate, useLocation } from '@solidjs/router'; +import { createEffect, Show } from 'solid-js'; import './index.css'; import AppContainer from './AppContainer'; import ItemExtractor from './ItemExtractor'; import TemplateCreator from './views/TemplateCreator'; import StandardPricing from './views/StandardPricing'; +import Login from './views/Login'; +import { authStore } from './store/authStore'; const root = document.getElementById('root'); @@ -14,8 +17,28 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) { ); } +function AuthGuard(props: { children: any }) { + const navigate = useNavigate(); + const location = useLocation(); + + createEffect(() => { + if (!authStore.isAuthenticated() && location.pathname !== '/login') { + navigate('/login', { replace: true }); + } else if (authStore.isAuthenticated() && location.pathname === '/login') { + navigate('/', { replace: true }); + } + }); + + return ( + + {props.children} + + ); +} + render(() => ( - + {props.children}}> + diff --git a/src/store/authStore.ts b/src/store/authStore.ts new file mode 100644 index 0000000..3b54ad2 --- /dev/null +++ b/src/store/authStore.ts @@ -0,0 +1,22 @@ +import { createSignal } from 'solid-js'; +import { pb } from '../utils/db'; + +const [isAuthenticated, setIsAuthenticated] = createSignal(pb.authStore.isValid); +const [currentUser, setCurrentUser] = createSignal(pb.authStore.model); + +// Listen for auth changes from PocketBase +pb.authStore.onChange((_, model) => { + setIsAuthenticated(pb.authStore.isValid); + setCurrentUser(model); +}); + +export const authStore = { + isAuthenticated, + currentUser, + login: async (email: string, pass: string) => { + return await pb.collection('users').authWithPassword(email, pass); + }, + logout: () => { + pb.authStore.clear(); + } +}; diff --git a/src/views/Login.tsx b/src/views/Login.tsx new file mode 100644 index 0000000..3abf883 --- /dev/null +++ b/src/views/Login.tsx @@ -0,0 +1,87 @@ +import { createSignal } from 'solid-js'; +import { useNavigate } from '@solidjs/router'; +import { authStore } from '../store/authStore'; + +export default function Login() { + const [email, setEmail] = createSignal(''); + const [password, setPassword] = createSignal(''); + const [error, setError] = createSignal(''); + const [isLoading, setIsLoading] = createSignal(false); + const navigate = useNavigate(); + + const handleLogin = async (e: Event) => { + e.preventDefault(); + setError(''); + setIsLoading(true); + + try { + await authStore.login(email(), password()); + // Successful login, navigate to home + navigate('/', { replace: true }); + } catch (err: any) { + console.error('Login error:', err); + setError(err.message || 'Failed to login. Please check your credentials.'); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+
+

+ Sign in to EstiMaker +

+
+
+ {error() && ( + + )} +
+
+ + setEmail(e.currentTarget.value)} + /> +
+
+ + setPassword(e.currentTarget.value)} + /> +
+
+ +
+ +
+
+
+
+ ); +}