Supplier based pricing added
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
import { createSignal, createMemo, onMount, Show } from 'solid-js';
|
import { createSignal, createMemo, onMount, Show, For } from 'solid-js';
|
||||||
import { FileText, ChevronDown, CheckCircle2, X } from 'lucide-solid';
|
import { FileText, ChevronDown, CheckCircle2, X } from 'lucide-solid';
|
||||||
import { Portal } from 'solid-js/web';
|
import { Portal } from 'solid-js/web';
|
||||||
|
|
||||||
@@ -66,6 +66,7 @@ const EstimateBuilder: Component<EstimateBuilderProps> = (props) => {
|
|||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!generalEstimateNotes()) setGeneralEstimateNotes(GENERAL_NOTES_DEFAULT);
|
if (!generalEstimateNotes()) setGeneralEstimateNotes(GENERAL_NOTES_DEFAULT);
|
||||||
if (!generalPreNotes()) setGeneralPreNotes(GENERAL_PRE_NOTES_DEFAULT);
|
if (!generalPreNotes()) setGeneralPreNotes(GENERAL_PRE_NOTES_DEFAULT);
|
||||||
|
appStore.loadSuppliers();
|
||||||
|
|
||||||
if (items.length === 0 && props.initialItems.length > 0) {
|
if (items.length === 0 && props.initialItems.length > 0) {
|
||||||
const transformed: EstimateItem[] = props.initialItems.map(item => ({
|
const transformed: EstimateItem[] = props.initialItems.map(item => ({
|
||||||
@@ -398,6 +399,11 @@ const EstimateBuilder: Component<EstimateBuilderProps> = (props) => {
|
|||||||
setShowNewEstimateModal(false);
|
setShowNewEstimateModal(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<datalist id="supplier-suggestions">
|
||||||
|
<For each={appStore.suppliers()}>
|
||||||
|
{(sup: string) => <option value={sup} />}
|
||||||
|
</For>
|
||||||
|
</datalist>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ interface ItemRowProps {
|
|||||||
onToggleSelection: (id: string, isMulti: boolean, isShift: boolean) => void;
|
onToggleSelection: (id: string, isMulti: boolean, isShift: boolean) => void;
|
||||||
onDuplicateItem: (id: string) => void;
|
onDuplicateItem: (id: string) => void;
|
||||||
hideColumns?: boolean;
|
hideColumns?: boolean;
|
||||||
|
activeSupplier?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ItemRow: Component<ItemRowProps> = (props) => {
|
const ItemRow: Component<ItemRowProps> = (props) => {
|
||||||
@@ -47,8 +48,15 @@ const ItemRow: Component<ItemRowProps> = (props) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
const filterParts = [`(name ~ "${searchString}" || category ~ "${searchString}")`];
|
||||||
|
if (props.activeSupplier) {
|
||||||
|
// Assuming exact match or contains? Let's use exact match for supplier
|
||||||
|
filterParts.push(`supplier = "${props.activeSupplier}"`);
|
||||||
|
}
|
||||||
|
const filterQuery = filterParts.join(' && ');
|
||||||
|
|
||||||
const records = await pb.collection(COLLECTIONS.STANDARD_PRICES).getList(1, 10, {
|
const records = await pb.collection(COLLECTIONS.STANDARD_PRICES).getList(1, 10, {
|
||||||
filter: `name ~ "${searchString}" || category ~ "${searchString}"`,
|
filter: filterQuery,
|
||||||
sort: 'category,name'
|
sort: 'category,name'
|
||||||
});
|
});
|
||||||
setSearchResults(records.items as unknown as StandardPrice[]);
|
setSearchResults(records.items as unknown as StandardPrice[]);
|
||||||
@@ -162,7 +170,12 @@ const ItemRow: Component<ItemRowProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<div class="text-xs font-bold text-gray-900">{result.name}</div>
|
<div class="text-xs font-bold text-gray-900">{result.name}</div>
|
||||||
<div class="flex justify-between items-center mt-0.5">
|
<div class="flex justify-between items-center mt-0.5">
|
||||||
<span class="text-[10px] text-gray-500 uppercase font-black tracking-wider">{result.category}</span>
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-[10px] text-gray-500 uppercase font-black tracking-wider">{result.category}</span>
|
||||||
|
<Show when={result.supplier}>
|
||||||
|
<span class="text-[9px] px-1.5 py-0.5 rounded-md bg-muted text-muted-foreground uppercase font-black tracking-widest">{result.supplier}</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
<span class="text-[11px] font-black text-primary">${result.price.toFixed(2)}</span>
|
<span class="text-[11px] font-black text-primary">${result.price.toFixed(2)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import NoteEditor from './NoteEditor';
|
|||||||
import LazyItem from './LazyItem';
|
import LazyItem from './LazyItem';
|
||||||
import ScopeScaleWrapper from './ui/ScopeScaleWrapper';
|
import ScopeScaleWrapper from './ui/ScopeScaleWrapper';
|
||||||
import { ModifierRegistry } from '../utils/modifier-registry';
|
import { ModifierRegistry } from '../utils/modifier-registry';
|
||||||
|
import { appStore } from '../store/appStore';
|
||||||
|
import { SupplierDropdown } from './ui/SupplierDropdown';
|
||||||
|
|
||||||
interface ScopeCardProps {
|
interface ScopeCardProps {
|
||||||
scope: Scope;
|
scope: Scope;
|
||||||
@@ -142,12 +144,18 @@ const ScopeCard: Component<ScopeCardProps> = (props) => {
|
|||||||
onInput={(e) => props.onUpdateScope(props.scope.id, { name: e.currentTarget.value })}
|
onInput={(e) => props.onUpdateScope(props.scope.id, { name: e.currentTarget.value })}
|
||||||
class="text-xl font-black text-foreground bg-transparent border-none outline-none focus:ring-0 w-64"
|
class="text-xl font-black text-foreground bg-transparent border-none outline-none focus:ring-0 w-64"
|
||||||
/>
|
/>
|
||||||
<div class="flex items-center gap-2 text-xs text-muted-foreground font-bold uppercase tracking-wider">
|
<div class="flex items-center gap-2 text-[10px] text-muted-foreground font-bold uppercase tracking-wider mt-1 h-6">
|
||||||
<span>{props.items.length} Items</span>
|
<span>{props.items.length} Items</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span>{formatQuantity(totals().totalQuantity)} Qty</span>
|
<span>{formatQuantity(totals().totalQuantity)} Qty</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span class="text-primary">${formatNumber(totals().total)} Total</span>
|
<span class="text-primary">${formatNumber(totals().total)} Total</span>
|
||||||
|
<span>•</span>
|
||||||
|
<SupplierDropdown
|
||||||
|
value={props.scope.supplier || ''}
|
||||||
|
options={appStore.suppliers()}
|
||||||
|
onSelect={(val) => props.onUpdateScope(props.scope.id, { supplier: val })}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -255,6 +263,7 @@ const ScopeCard: Component<ScopeCardProps> = (props) => {
|
|||||||
anySelected={props.selectedIds.length > 0}
|
anySelected={props.selectedIds.length > 0}
|
||||||
onToggleSelection={props.onToggleSelection}
|
onToggleSelection={props.onToggleSelection}
|
||||||
hideColumns={props.hideColumns}
|
hideColumns={props.hideColumns}
|
||||||
|
activeSupplier={props.scope.supplier}
|
||||||
/>
|
/>
|
||||||
</LazyItem>
|
</LazyItem>
|
||||||
)}
|
)}
|
||||||
@@ -291,6 +300,7 @@ const ScopeCard: Component<ScopeCardProps> = (props) => {
|
|||||||
onToggleSelection={props.onToggleSelection}
|
onToggleSelection={props.onToggleSelection}
|
||||||
onClearSelection={props.onClearSelection}
|
onClearSelection={props.onClearSelection}
|
||||||
hideColumns={props.hideColumns}
|
hideColumns={props.hideColumns}
|
||||||
|
scopeSupplier={props.scope.supplier}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ interface SubScopeCardProps {
|
|||||||
onToggleSelection: (id: string, isMulti: boolean, isShift: boolean) => void;
|
onToggleSelection: (id: string, isMulti: boolean, isShift: boolean) => void;
|
||||||
onClearSelection: () => void;
|
onClearSelection: () => void;
|
||||||
hideColumns?: boolean;
|
hideColumns?: boolean;
|
||||||
|
scopeSupplier?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SubScopeCard: Component<SubScopeCardProps> = (props) => {
|
const SubScopeCard: Component<SubScopeCardProps> = (props) => {
|
||||||
@@ -218,6 +219,7 @@ const SubScopeCard: Component<SubScopeCardProps> = (props) => {
|
|||||||
anySelected={props.selectedIds.length > 0}
|
anySelected={props.selectedIds.length > 0}
|
||||||
onToggleSelection={props.onToggleSelection}
|
onToggleSelection={props.onToggleSelection}
|
||||||
hideColumns={props.hideColumns}
|
hideColumns={props.hideColumns}
|
||||||
|
activeSupplier={props.scopeSupplier}
|
||||||
/>
|
/>
|
||||||
</LazyItem>
|
</LazyItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { createSignal, Show, For, onMount, onCleanup } from 'solid-js';
|
||||||
|
import { Portal } from 'solid-js/web';
|
||||||
|
import { ChevronDown, Check } from 'lucide-solid';
|
||||||
|
|
||||||
|
interface SupplierDropdownProps {
|
||||||
|
value: string;
|
||||||
|
options: string[];
|
||||||
|
onSelect: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SupplierDropdown = (props: SupplierDropdownProps) => {
|
||||||
|
const [isOpen, setIsOpen] = createSignal(false);
|
||||||
|
let containerRef: HTMLDivElement | undefined;
|
||||||
|
let buttonRef: HTMLButtonElement | undefined;
|
||||||
|
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
if (containerRef && !containerRef.contains(e.target as Node) &&
|
||||||
|
buttonRef && !buttonRef.contains(e.target as Node)) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
document.addEventListener('click', handleClickOutside);
|
||||||
|
});
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
document.removeEventListener('click', handleClickOutside);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getRect = () => buttonRef?.getBoundingClientRect();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="relative inline-block">
|
||||||
|
<button
|
||||||
|
ref={buttonRef}
|
||||||
|
onClick={() => setIsOpen(!isOpen())}
|
||||||
|
class="flex items-center gap-1.5 group/supplier bg-zinc-100/50 hover:bg-zinc-100 border border-zinc-200/50 rounded-full px-3 py-1 transition-all duration-200 focus:outline-none focus:ring-4 focus:ring-primary/10 focus:border-primary/30 active:scale-95"
|
||||||
|
>
|
||||||
|
<span class="text-[9px] font-black uppercase tracking-widest text-muted-foreground/60 select-none">Supplier</span>
|
||||||
|
<span class="text-[9px] font-black uppercase tracking-widest text-foreground min-w-[30px] text-left">
|
||||||
|
{props.value || props.placeholder || 'Any'}
|
||||||
|
</span>
|
||||||
|
<ChevronDown
|
||||||
|
class={`w-2.5 h-2.5 text-muted-foreground transition-transform duration-300 ${isOpen() ? 'rotate-180' : 'group-hover/supplier:translate-y-0.5'}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Show when={isOpen()}>
|
||||||
|
<Portal>
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
class="fixed z-[1000] min-w-[160px] bg-white/90 backdrop-blur-xl border border-white/20 rounded-2xl shadow-2xl shadow-zinc-900/10 overflow-hidden animate-in fade-in zoom-in-95 duration-200 origin-top"
|
||||||
|
style={{
|
||||||
|
top: `${(getRect()?.bottom || 0) + 8}px`,
|
||||||
|
left: `${getRect()?.left || 0}px`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="p-1.5 flex flex-col gap-0.5">
|
||||||
|
<button
|
||||||
|
onClick={() => { props.onSelect(''); setIsOpen(false); }}
|
||||||
|
class={`w-full flex items-center justify-between px-3 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all ${!props.value ? 'bg-primary text-primary-foreground shadow-lg shadow-primary/20' : 'hover:bg-zinc-100 text-muted-foreground/60'}`}
|
||||||
|
>
|
||||||
|
<span>Any</span>
|
||||||
|
<Show when={!props.value}>
|
||||||
|
<Check class="w-3 h-3" />
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
<div class="h-px bg-zinc-100 my-0.5 mx-1"></div>
|
||||||
|
<For each={props.options}>
|
||||||
|
{(option) => (
|
||||||
|
<button
|
||||||
|
onClick={() => { props.onSelect(option); setIsOpen(false); }}
|
||||||
|
class={`w-full flex items-center justify-between px-3 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all ${props.value === option ? 'bg-primary text-primary-foreground shadow-lg shadow-primary/20' : 'hover:bg-zinc-100 text-foreground'}`}
|
||||||
|
>
|
||||||
|
<span>{option}</span>
|
||||||
|
<Show when={props.value === option}>
|
||||||
|
<Check class="w-3 h-3" />
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Portal>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,6 +7,7 @@ import { ModifierRegistry } from '../utils/modifier-registry';
|
|||||||
// Raw CSV Data
|
// Raw CSV Data
|
||||||
const [activeItems, setActiveItems] = createSignal<ExtractedItem[]>([]);
|
const [activeItems, setActiveItems] = createSignal<ExtractedItem[]>([]);
|
||||||
const [activeIgnoredLines, setActiveIgnoredLines] = createSignal<string[][]>([]);
|
const [activeIgnoredLines, setActiveIgnoredLines] = createSignal<string[][]>([]);
|
||||||
|
const [suppliers, setSuppliers] = createSignal<string[]>([]);
|
||||||
|
|
||||||
// Estimate Metadata
|
// Estimate Metadata
|
||||||
const [activeEstimateId, setActiveEstimateId] = createSignal<string | null>(null);
|
const [activeEstimateId, setActiveEstimateId] = createSignal<string | null>(null);
|
||||||
@@ -44,6 +45,8 @@ const [activeTemplateModifiers, setActiveTemplateModifiers] = createStore<Modifi
|
|||||||
const [activeTemplateSubScopeName, setActiveTemplateSubScopeName] = createSignal<string | undefined>(undefined);
|
const [activeTemplateSubScopeName, setActiveTemplateSubScopeName] = createSignal<string | undefined>(undefined);
|
||||||
const [activeTemplateId, setActiveTemplateId] = createSignal<string | null>(null);
|
const [activeTemplateId, setActiveTemplateId] = createSignal<string | null>(null);
|
||||||
|
|
||||||
|
import { pb, COLLECTIONS } from '../utils/db';
|
||||||
|
|
||||||
export const appStore = {
|
export const appStore = {
|
||||||
// Raw CSV Extraction
|
// Raw CSV Extraction
|
||||||
items: activeItems,
|
items: activeItems,
|
||||||
@@ -162,5 +165,19 @@ export const appStore = {
|
|||||||
setActiveTemplateModifiers([]);
|
setActiveTemplateModifiers([]);
|
||||||
setActiveTemplateSubScopeName(undefined);
|
setActiveTemplateSubScopeName(undefined);
|
||||||
setActiveTemplateId(null);
|
setActiveTemplateId(null);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Suppliers
|
||||||
|
suppliers,
|
||||||
|
async loadSuppliers() {
|
||||||
|
try {
|
||||||
|
const records = await pb.collection(COLLECTIONS.STANDARD_PRICES).getFullList({
|
||||||
|
fields: 'supplier'
|
||||||
|
});
|
||||||
|
const unique = [...new Set(records.map(r => r.supplier).filter(Boolean))].sort() as string[];
|
||||||
|
setSuppliers(unique);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading suppliers:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+2
-1
@@ -13,6 +13,7 @@ export interface Scope {
|
|||||||
useDeliveryLogic: 'percent' | 'simple';
|
useDeliveryLogic: 'percent' | 'simple';
|
||||||
estimatorNotes?: string;
|
estimatorNotes?: string;
|
||||||
bidDescription?: string;
|
bidDescription?: string;
|
||||||
|
supplier?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ModifierType = 'tax' | 'man-hours' | 'custom';
|
export type ModifierType = 'tax' | 'man-hours' | 'custom';
|
||||||
@@ -81,5 +82,5 @@ export interface StandardPrice {
|
|||||||
category: string;
|
category: string;
|
||||||
name: string;
|
name: string;
|
||||||
price: number;
|
price: number;
|
||||||
priceRange?: string;
|
supplier?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { createSignal, onMount, For, createMemo } from 'solid-js';
|
import { createSignal, onMount, For, createMemo } from 'solid-js';
|
||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
import { Trash2, Plus, RefreshCw, Edit2, Save, X } from 'lucide-solid';
|
import { Trash2, Plus, RefreshCw, Edit2, Save, X, ChevronDown } from 'lucide-solid';
|
||||||
import { pb, COLLECTIONS } from '../utils/db';
|
import { pb, COLLECTIONS } from '../utils/db';
|
||||||
import type { StandardPrice } from '../types';
|
import type { StandardPrice } from '../types';
|
||||||
|
import { appStore } from '../store/appStore';
|
||||||
|
|
||||||
const StandardPricing: Component = () => {
|
const StandardPricing: Component = () => {
|
||||||
const [prices, setPrices] = createSignal<StandardPrice[]>([]);
|
const [prices, setPrices] = createSignal<StandardPrice[]>([]);
|
||||||
@@ -11,13 +12,13 @@ const StandardPricing: Component = () => {
|
|||||||
const [newCategory, setNewCategory] = createSignal('');
|
const [newCategory, setNewCategory] = createSignal('');
|
||||||
const [newName, setNewName] = createSignal('');
|
const [newName, setNewName] = createSignal('');
|
||||||
const [newPrice, setNewPrice] = createSignal<number>(0);
|
const [newPrice, setNewPrice] = createSignal<number>(0);
|
||||||
const [newPriceRange, setNewPriceRange] = createSignal('');
|
const [newSupplier, setNewSupplier] = createSignal('');
|
||||||
|
|
||||||
const [editingId, setEditingId] = createSignal<string | null>(null);
|
const [editingId, setEditingId] = createSignal<string | null>(null);
|
||||||
const [editCategory, setEditCategory] = createSignal('');
|
const [editCategory, setEditCategory] = createSignal('');
|
||||||
const [editName, setEditName] = createSignal('');
|
const [editName, setEditName] = createSignal('');
|
||||||
const [editPrice, setEditPrice] = createSignal<number>(0);
|
const [editPrice, setEditPrice] = createSignal<number>(0);
|
||||||
const [editPriceRange, setEditPriceRange] = createSignal('');
|
const [editSupplier, setEditSupplier] = createSignal('');
|
||||||
|
|
||||||
const categories = createMemo(() => {
|
const categories = createMemo(() => {
|
||||||
return [...new Set(prices().map(p => p.category))].sort();
|
return [...new Set(prices().map(p => p.category))].sort();
|
||||||
@@ -43,6 +44,7 @@ const StandardPricing: Component = () => {
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
loadPrices();
|
loadPrices();
|
||||||
|
appStore.loadSuppliers();
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleAddPrice = async () => {
|
const handleAddPrice = async () => {
|
||||||
@@ -53,15 +55,16 @@ const StandardPricing: Component = () => {
|
|||||||
category: newCategory(),
|
category: newCategory(),
|
||||||
name: newName(),
|
name: newName(),
|
||||||
price: newPrice(),
|
price: newPrice(),
|
||||||
priceRange: newPriceRange()
|
supplier: newSupplier()
|
||||||
};
|
};
|
||||||
const added = await pb.collection(COLLECTIONS.STANDARD_PRICES).create(record);
|
const added = await pb.collection(COLLECTIONS.STANDARD_PRICES).create(record);
|
||||||
setPrices([...prices(), added as unknown as StandardPrice]);
|
setPrices([...prices(), added as unknown as StandardPrice]);
|
||||||
|
appStore.loadSuppliers(); // Refresh global list
|
||||||
|
|
||||||
// Reset fields
|
// Reset fields
|
||||||
setNewName('');
|
setNewName('');
|
||||||
setNewPrice(0);
|
setNewPrice(0);
|
||||||
setNewPriceRange('');
|
setNewSupplier('');
|
||||||
// Keep category the same to easily add multiple items to same category
|
// Keep category the same to easily add multiple items to same category
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error adding price:', error);
|
console.error('Error adding price:', error);
|
||||||
@@ -74,7 +77,7 @@ const StandardPricing: Component = () => {
|
|||||||
setEditCategory(price.category);
|
setEditCategory(price.category);
|
||||||
setEditName(price.name);
|
setEditName(price.name);
|
||||||
setEditPrice(price.price);
|
setEditPrice(price.price);
|
||||||
setEditPriceRange(price.priceRange || '');
|
setEditSupplier(price.supplier || '');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditCancel = () => {
|
const handleEditCancel = () => {
|
||||||
@@ -90,11 +93,12 @@ const StandardPricing: Component = () => {
|
|||||||
category: editCategory(),
|
category: editCategory(),
|
||||||
name: editName(),
|
name: editName(),
|
||||||
price: editPrice(),
|
price: editPrice(),
|
||||||
priceRange: editPriceRange()
|
supplier: editSupplier()
|
||||||
};
|
};
|
||||||
await pb.collection(COLLECTIONS.STANDARD_PRICES).update(id, updates);
|
await pb.collection(COLLECTIONS.STANDARD_PRICES).update(id, updates);
|
||||||
setPrices(prices().map(p => p.id === id ? { ...p, ...updates } : p));
|
setPrices(prices().map(p => p.id === id ? { ...p, ...updates } : p));
|
||||||
setEditingId(null);
|
setEditingId(null);
|
||||||
|
appStore.loadSuppliers(); // Refresh global list
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating price:', error);
|
console.error('Error updating price:', error);
|
||||||
alert('Failed to update standard price.');
|
alert('Failed to update standard price.');
|
||||||
@@ -106,6 +110,7 @@ const StandardPricing: Component = () => {
|
|||||||
try {
|
try {
|
||||||
await pb.collection(COLLECTIONS.STANDARD_PRICES).delete(id);
|
await pb.collection(COLLECTIONS.STANDARD_PRICES).delete(id);
|
||||||
setPrices(prices().filter(p => p.id !== id));
|
setPrices(prices().filter(p => p.id !== id));
|
||||||
|
appStore.loadSuppliers(); // Refresh global list
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting price:', error);
|
console.error('Error deleting price:', error);
|
||||||
alert('Failed to delete standard price.');
|
alert('Failed to delete standard price.');
|
||||||
@@ -135,56 +140,64 @@ const StandardPricing: Component = () => {
|
|||||||
<h3 class="text-sm font-bold text-blue-800 mb-4 uppercase tracking-wider">Add New Standard Price</h3>
|
<h3 class="text-sm font-bold text-blue-800 mb-4 uppercase tracking-wider">Add New Standard Price</h3>
|
||||||
<div class="flex items-end gap-4 overflow-x-auto pb-2">
|
<div class="flex items-end gap-4 overflow-x-auto pb-2">
|
||||||
<div class="flex-1 min-w-[200px]">
|
<div class="flex-1 min-w-[200px]">
|
||||||
<label class="block text-xs font-bold text-gray-600 mb-1">Category</label>
|
<label class="block text-[10px] font-black tracking-widest text-zinc-400 mb-1.5 uppercase pl-1">Category</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={newCategory()}
|
value={newCategory()}
|
||||||
onInput={(e) => setNewCategory(e.currentTarget.value)}
|
onInput={(e) => setNewCategory(e.currentTarget.value)}
|
||||||
placeholder="e.g. Drywall"
|
placeholder="e.g. Drywall"
|
||||||
list="category-suggestions"
|
list="category-suggestions"
|
||||||
class="w-full bg-white border border-gray-200 rounded-lg px-3 py-2 text-sm focus:border-blue-500 outline-none"
|
class="w-full bg-zinc-50/50 border border-zinc-200/60 rounded-xl px-4 py-2.5 text-sm focus:bg-white focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 outline-none transition-all duration-200 shadow-sm shadow-zinc-200/50 placeholder:text-zinc-300"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-[200px]">
|
<div class="flex-1 min-w-[200px]">
|
||||||
<label class="block text-xs font-bold text-gray-600 mb-1">Item Name</label>
|
<label class="block text-[10px] font-black tracking-widest text-zinc-400 mb-1.5 uppercase pl-1">Item Name</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={newName()}
|
value={newName()}
|
||||||
onInput={(e) => setNewName(e.currentTarget.value)}
|
onInput={(e) => setNewName(e.currentTarget.value)}
|
||||||
placeholder="e.g. Gyp Type X"
|
placeholder="e.g. Gyp Type X"
|
||||||
list="name-suggestions"
|
list="name-suggestions"
|
||||||
class="w-full bg-white border border-gray-200 rounded-lg px-3 py-2 text-sm focus:border-blue-500 outline-none"
|
class="w-full bg-zinc-50/50 border border-zinc-200/60 rounded-xl px-4 py-2.5 text-sm focus:bg-white focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 outline-none transition-all duration-200 shadow-sm shadow-zinc-200/50 placeholder:text-zinc-300"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-32 shrink-0">
|
<div class="w-32 shrink-0">
|
||||||
<label class="block text-xs font-bold text-gray-600 mb-1">Price ($)</label>
|
<label class="block text-[10px] font-black tracking-widest text-zinc-400 mb-1.5 uppercase pl-1">Price ($)</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
value={newPrice() || ''}
|
value={newPrice() || ''}
|
||||||
onInput={(e) => setNewPrice(parseFloat(e.currentTarget.value) || 0)}
|
onInput={(e) => setNewPrice(parseFloat(e.currentTarget.value) || 0)}
|
||||||
placeholder="0.50"
|
placeholder="0.00"
|
||||||
class="w-full bg-white border border-gray-200 rounded-lg px-3 py-2 text-sm focus:border-blue-500 outline-none text-right font-bold"
|
class="w-full bg-zinc-50/50 border border-zinc-200/60 rounded-xl px-4 py-2.5 text-sm focus:bg-white focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 outline-none text-right font-black transition-all duration-200 shadow-sm shadow-zinc-200/50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-32 shrink-0">
|
<div class="w-40 shrink-0">
|
||||||
<label class="block text-xs font-bold text-gray-600 mb-1">Range (Opt)</label>
|
<label class="block text-[10px] font-black tracking-widest text-zinc-400 mb-1.5 uppercase pl-1">Supplier (Opt)</label>
|
||||||
<input
|
<div class="relative group/sel">
|
||||||
type="text"
|
<input
|
||||||
value={newPriceRange()}
|
type="text"
|
||||||
onInput={(e) => setNewPriceRange(e.currentTarget.value)}
|
value={newSupplier()}
|
||||||
placeholder="0.45-0.80"
|
onInput={(e) => setNewSupplier(e.currentTarget.value)}
|
||||||
class="w-full bg-white border border-gray-200 rounded-lg px-3 py-2 text-sm focus:border-blue-500 outline-none"
|
placeholder="Any..."
|
||||||
/>
|
list="supplier-suggestions"
|
||||||
|
class="w-full bg-zinc-50/50 border border-zinc-200/60 rounded-xl px-4 py-2.5 text-sm focus:bg-white focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 outline-none pr-10 cursor-pointer transition-all duration-200 shadow-sm shadow-zinc-200/50 placeholder:text-zinc-300 hover:border-zinc-300"
|
||||||
|
/>
|
||||||
|
<div class="absolute right-3.5 top-1/2 -translate-y-1/2 pointer-events-none text-zinc-300 group-hover/sel:text-blue-500 transition-colors">
|
||||||
|
<ChevronDown class="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-end h-[74px]">
|
||||||
|
<button
|
||||||
|
onClick={handleAddPrice}
|
||||||
|
disabled={!newCategory() || !newName() || newPrice() < 0}
|
||||||
|
class="flex items-center gap-2 px-6 py-2.5 bg-blue-600 text-white rounded-xl font-black text-sm hover:bg-blue-700 hover:shadow-xl hover:shadow-blue-500/25 active:scale-95 active:shadow-inner disabled:opacity-30 disabled:pointer-events-none transition-all duration-200 h-[46px] shadow-lg shadow-blue-500/20"
|
||||||
|
>
|
||||||
|
<Plus class="w-4 h-4 transition-transform group-hover:rotate-90" />
|
||||||
|
<span>Add Item</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
onClick={handleAddPrice}
|
|
||||||
disabled={!newCategory() || !newName() || newPrice() < 0}
|
|
||||||
class="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg font-bold hover:bg-blue-700 disabled:opacity-50 transition-colors shrink-0 h-[38px]"
|
|
||||||
>
|
|
||||||
<Plus class="w-4 h-4" />
|
|
||||||
Add
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -206,7 +219,7 @@ const StandardPricing: Component = () => {
|
|||||||
<th class="py-3 px-4">Category</th>
|
<th class="py-3 px-4">Category</th>
|
||||||
<th class="py-3 px-4">Item Name</th>
|
<th class="py-3 px-4">Item Name</th>
|
||||||
<th class="py-3 px-4 text-right">Price</th>
|
<th class="py-3 px-4 text-right">Price</th>
|
||||||
<th class="py-3 px-4">Range</th>
|
<th class="py-3 px-4">Supplier</th>
|
||||||
<th class="py-3 px-4 w-24"></th>
|
<th class="py-3 px-4 w-24"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -221,7 +234,7 @@ const StandardPricing: Component = () => {
|
|||||||
value={editCategory()}
|
value={editCategory()}
|
||||||
onInput={(e) => setEditCategory(e.currentTarget.value)}
|
onInput={(e) => setEditCategory(e.currentTarget.value)}
|
||||||
list="category-suggestions"
|
list="category-suggestions"
|
||||||
class="w-full bg-white border border-gray-200 rounded-lg px-2 py-1 text-xs focus:border-blue-500 outline-none"
|
class="w-full bg-white border border-zinc-200/60 rounded-xl px-3 py-1.5 text-xs focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 outline-none transition-all duration-200 shadow-sm"
|
||||||
/>
|
/>
|
||||||
) : price.category}
|
) : price.category}
|
||||||
</td>
|
</td>
|
||||||
@@ -232,7 +245,7 @@ const StandardPricing: Component = () => {
|
|||||||
value={editName()}
|
value={editName()}
|
||||||
onInput={(e) => setEditName(e.currentTarget.value)}
|
onInput={(e) => setEditName(e.currentTarget.value)}
|
||||||
list="name-suggestions"
|
list="name-suggestions"
|
||||||
class="w-full bg-white border border-gray-200 rounded-lg px-2 py-1 text-xs focus:border-blue-500 outline-none"
|
class="w-full bg-white border border-zinc-200/60 rounded-xl px-3 py-1.5 text-xs focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 outline-none transition-all duration-200 shadow-sm"
|
||||||
/>
|
/>
|
||||||
) : price.name}
|
) : price.name}
|
||||||
</td>
|
</td>
|
||||||
@@ -243,19 +256,25 @@ const StandardPricing: Component = () => {
|
|||||||
step="0.01"
|
step="0.01"
|
||||||
value={editPrice()}
|
value={editPrice()}
|
||||||
onInput={(e) => setEditPrice(parseFloat(e.currentTarget.value) || 0)}
|
onInput={(e) => setEditPrice(parseFloat(e.currentTarget.value) || 0)}
|
||||||
class="w-24 bg-white border border-gray-200 rounded-lg px-2 py-1 text-xs focus:border-blue-500 outline-none text-right font-bold"
|
class="w-24 bg-white border border-zinc-200/60 rounded-xl px-3 py-1.5 text-xs focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 outline-none text-right font-black transition-all duration-200 shadow-sm"
|
||||||
/>
|
/>
|
||||||
) : `$${price.price.toFixed(2)}`}
|
) : `$${price.price.toFixed(2)}`}
|
||||||
</td>
|
</td>
|
||||||
<td class="py-3 px-4 text-sm text-gray-500">
|
<td class="py-3 px-4 text-sm text-gray-500">
|
||||||
{editingId() === price.id ? (
|
{editingId() === price.id ? (
|
||||||
<input
|
<div class="relative group/sel">
|
||||||
type="text"
|
<input
|
||||||
value={editPriceRange()}
|
type="text"
|
||||||
onInput={(e) => setEditPriceRange(e.currentTarget.value)}
|
value={editSupplier()}
|
||||||
class="w-full bg-white border border-gray-200 rounded-lg px-2 py-1 text-xs focus:border-blue-500 outline-none"
|
onInput={(e) => setEditSupplier(e.currentTarget.value)}
|
||||||
/>
|
list="supplier-suggestions"
|
||||||
) : (price.priceRange || '-')}
|
class="w-full bg-white border border-zinc-200/60 rounded-xl px-3 py-1.5 text-xs focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 outline-none pr-8 cursor-pointer transition-all duration-200 shadow-sm"
|
||||||
|
/>
|
||||||
|
<div class="absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none text-zinc-300 group-hover/sel:text-blue-500 transition-colors">
|
||||||
|
<ChevronDown class="w-3 h-3" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (price.supplier || '-')}
|
||||||
</td>
|
</td>
|
||||||
<td class="py-3 px-4 text-right">
|
<td class="py-3 px-4 text-right">
|
||||||
<div class="flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
<div class="flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
@@ -317,6 +336,11 @@ const StandardPricing: Component = () => {
|
|||||||
{(name) => <option value={name} />}
|
{(name) => <option value={name} />}
|
||||||
</For>
|
</For>
|
||||||
</datalist>
|
</datalist>
|
||||||
|
<datalist id="supplier-suggestions">
|
||||||
|
<For each={appStore.suppliers()}>
|
||||||
|
{(sup) => <option value={sup} />}
|
||||||
|
</For>
|
||||||
|
</datalist>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user