From 0d97a497ea2bf657db005095d4a0e6846c578ac4 Mon Sep 17 00:00:00 2001
From: Jurgis Sakalauskas
Date: Tue, 14 Oct 2025 14:27:28 +0300
Subject: [PATCH] prices panel
---
client/src/api/item-prices.ts | 6 +
.../project-detail/item-prices-modal.tsx | 67 ----
.../project-detail/item-prices-panel.tsx | 369 ++++++++++--------
client/src/query/item-prices.ts | 21 +
4 files changed, 244 insertions(+), 219 deletions(-)
delete mode 100644 client/src/pages/project-detail/item-prices-modal.tsx
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}
-
+ {errorMessage ? (
+ {errorMessage}
+ ) : null}
+
);
}
diff --git a/client/src/query/item-prices.ts b/client/src/query/item-prices.ts
index 939a88f..dc30fc6 100644
--- a/client/src/query/item-prices.ts
+++ b/client/src/query/item-prices.ts
@@ -1,6 +1,7 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
createItemPrice,
+ deleteItemPrice,
fetchItemPrices,
type CreateItemPricePayload,
type ItemPricesResponse
@@ -50,3 +51,23 @@ export const useCreateItemPriceMutation = (
}
});
};
+
+export const useDeleteItemPriceMutation = (
+ itemId: string | undefined,
+ projectId: string | undefined
+) => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (priceId: string) => deleteItemPrice(priceId),
+ onSuccess: () => {
+ if (itemId) {
+ queryClient.invalidateQueries({ queryKey: itemPricesKeys.list(itemId) });
+ }
+ if (projectId) {
+ queryClient.invalidateQueries({ queryKey: projectsKeys.items(projectId) });
+ queryClient.invalidateQueries({ queryKey: projectsKeys.detail(projectId) });
+ }
+ }
+ });
+};