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 (
+
+ );
+}