diff --git a/client/src/api/item-prices.ts b/client/src/api/item-prices.ts index 41bf57e..fe940a2 100644 --- a/client/src/api/item-prices.ts +++ b/client/src/api/item-prices.ts @@ -73,3 +73,9 @@ export const createItemPrice = async ( return response.data; }; + +export const deleteItemPrice = async (priceId: string): Promise => { + await apiFetch(`/prices/${priceId}`, { + method: 'DELETE' + }); +}; diff --git a/client/src/pages/project-detail/item-prices-modal.tsx b/client/src/pages/project-detail/item-prices-modal.tsx deleted file mode 100644 index 1f96afb..0000000 --- a/client/src/pages/project-detail/item-prices-modal.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { useEffect } from 'react'; -import type { Item } from '@shared/models/item'; -import { ItemPricesPanel } from './item-prices-panel'; - -interface ItemPricesModalProps { - isOpen: boolean; - item: Item | null; - projectId: string | undefined; - onClose: () => void; -} - -export function ItemPricesModal({ - isOpen, - item, - projectId, - onClose -}: ItemPricesModalProps) { - useEffect(() => { - if (!isOpen) { - return; - } - - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Escape') { - onClose(); - } - }; - - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [isOpen, onClose]); - - if (!isOpen || !item) { - return null; - } - - const itemTitle = item.manufacturer ? `${item.manufacturer} ${item.model}` : item.model; - - return ( -
-
event.stopPropagation()} - > - -
-
-

Edit Item Prices

-

{itemTitle}

-
- -
-
-
- ); -} diff --git a/client/src/pages/project-detail/item-prices-panel.tsx b/client/src/pages/project-detail/item-prices-panel.tsx index 796dce8..0f7963b 100644 --- a/client/src/pages/project-detail/item-prices-panel.tsx +++ b/client/src/pages/project-detail/item-prices-panel.tsx @@ -1,6 +1,12 @@ import { FormEvent, useMemo, useState } from 'react'; -import { useCreateItemPriceMutation, useItemPricesQuery } from '../../query/item-prices'; +import { Trash2 } from 'lucide-react'; +import { + useCreateItemPriceMutation, + useDeleteItemPriceMutation, + useItemPricesQuery +} from '../../query/item-prices'; import type { PriceCondition } from '../../api/item-prices'; +import { formatRelativeTime } from '../../utils/relative-time'; const priceConditionOptions: PriceCondition[] = ['new', 'used']; @@ -12,23 +18,39 @@ interface ItemPricesPanelProps { export function ItemPricesPanel({ itemId, projectId }: ItemPricesPanelProps) { const [condition, setCondition] = useState('new'); const [amount, setAmount] = useState(''); - const [currency, setCurrency] = useState('USD'); + const [currency, setCurrency] = useState('EUR'); const [sourceUrl, setSourceUrl] = useState(''); const [note, setNote] = useState(''); const [formError, setFormError] = useState(null); + const [actionError, setActionError] = useState(null); + const [pendingDeletionId, setPendingDeletionId] = useState(null); const pricesQuery = useItemPricesQuery(itemId, true); const createPriceMutation = useCreateItemPriceMutation(itemId, projectId); + const deletePriceMutation = useDeleteItemPriceMutation(itemId, projectId); const prices = pricesQuery.data?.data ?? []; - const creationError = useMemo( - () => formError ?? (createPriceMutation.isError ? createPriceMutation.error.message : null), - [formError, createPriceMutation.isError, createPriceMutation.error] + const errorMessage = useMemo( + () => + formError ?? + actionError ?? + (createPriceMutation.isError ? createPriceMutation.error.message : null) ?? + (deletePriceMutation.isError ? deletePriceMutation.error.message : null), + [ + formError, + actionError, + createPriceMutation.isError, + createPriceMutation.error, + deletePriceMutation.isError, + deletePriceMutation.error + ] ); const handleSubmit = async (event: FormEvent) => { event.preventDefault(); setFormError(null); + setActionError(null); + createPriceMutation.reset(); const parsedAmount = Number(amount); @@ -50,7 +72,21 @@ export function ItemPricesPanel({ itemId, projectId }: ItemPricesPanelProps) { setSourceUrl(''); setNote(''); } catch (error) { - setFormError((error as Error).message); + setActionError((error as Error).message); + } + }; + + const handleDeletePrice = async (priceId: string) => { + setActionError(null); + deletePriceMutation.reset(); + setPendingDeletionId(priceId); + + try { + await deletePriceMutation.mutateAsync(priceId); + } catch (error) { + setActionError((error as Error).message); + } finally { + setPendingDeletionId(null); } }; @@ -78,154 +114,183 @@ export function ItemPricesPanel({ itemId, projectId }: ItemPricesPanelProps) {

) : null} -
-
-
-
- - -
-
- - setAmount(event.target.value)} - className="rounded-md border border-slate-300 px-2 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200" - placeholder="129.99" - required - /> -
-
+ +
+ + + + + + + + + + + + + + {pricesQuery.isLoading ? ( + + + + ) : prices.length ? ( + prices.map((price) => { + const lastUpdated = price.updatedAt ?? price.createdAt; -
- - setCurrency(event.target.value.toUpperCase())} - maxLength={3} - className="uppercase rounded-md border border-slate-300 px-2 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200" - placeholder="USD" - required - /> -
- -
- - setSourceUrl(event.target.value)} - className="rounded-md border border-slate-300 px-2 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200" - placeholder="https://shop.example.com/deal" - /> -
- -
- -
+ Condition + + Amount + + Currency + + Source + + Notes + + Updated + + Actions +
+ Loading prices… +