Skip to content

Commit d7693d5

Browse files
authored
GPT-36: Price History (#22)
* price history table done, no chart yet * price history graph * add item now shows price history
1 parent e0b3ef9 commit d7693d5

10 files changed

Lines changed: 287 additions & 16 deletions

File tree

amplify/data/resource.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,23 @@ const schema = a.schema({
2020
storeId: a.id().required(),
2121
isDiscount: a.boolean(),
2222
discountedPrice: a.float(),
23+
priceHistory: a.hasMany("PriceHistory", "itemId"),
2324
})
2425
.secondaryIndexes((index) => [
2526
index("barcode"),
2627
index("itemName"),
2728
])
2829
.authorization((allow) => [allow.guest()]),
30+
PriceHistory: a.
31+
model({
32+
itemId: a.id().required(),
33+
itemName: a.string().required(),
34+
price: a.float().required(),
35+
discountedPrice: a.float(),
36+
changedAt: a.string().required(),
37+
item: a.belongsTo("Item", "itemId"),
38+
})
39+
.authorization((allow) => [allow.guest()]),
2940
StoreLocation: a
3041
.customType({
3142
streetName: a.string().required(),

package-lock.json

Lines changed: 52 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717
"@fortawesome/free-solid-svg-icons": "^6.7.2",
1818
"@fortawesome/react-fontawesome": "^0.2.2",
1919
"aws-amplify": "^6.15.3",
20+
"chart.js": "^4.5.0",
21+
"chartjs-adapter-date-fns": "^3.0.0",
2022
"dompurify": "^3.2.6",
2123
"react": "^19.1.0",
24+
"react-chartjs-2": "^5.3.0",
2225
"react-dom": "^19.1.0",
2326
"react-dropdown-select": "^4.12.2",
2427
"react-qr-barcode-scanner": "^2.1.8",

src/components/ItemCard.tsx

Lines changed: 161 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
11
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
22
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
33
import { useNavigate } from "react-router-dom";
4+
import type { PriceHistory } from "../types/priceHistory";
5+
import 'chartjs-adapter-date-fns';
6+
import { Line } from 'react-chartjs-2';
7+
import {
8+
Chart as ChartJS,
9+
LineElement,
10+
PointElement,
11+
CategoryScale,
12+
LinearScale,
13+
Tooltip,
14+
Legend,
15+
TimeScale,
16+
type ChartOptions,
17+
} from 'chart.js';
18+
19+
ChartJS.register(LineElement, PointElement, CategoryScale, LinearScale, Tooltip, Legend, TimeScale);
20+
421

522
export interface ItemCardProps {
623
id: string;
@@ -13,6 +30,7 @@ export interface ItemCardProps {
1330
isDiscount: boolean;
1431
itemPrice: number;
1532
discountedPrice?: number;
33+
priceHistory?: PriceHistory[];
1634
}
1735

1836
const ItemCard: React.FC<ItemCardProps> = ({
@@ -26,13 +44,106 @@ const ItemCard: React.FC<ItemCardProps> = ({
2644
isDiscount,
2745
itemPrice,
2846
discountedPrice,
47+
priceHistory
2948
}) => {
3049
const navigate = useNavigate();
3150

3251
const handleEditClick = () => {
3352
navigate(`/edit-item/${id}`);
3453
};
3554

55+
const formatPrice = (price: number) => {
56+
return price % 1 === 0 ? `${price}` : `${price.toFixed(2)}`
57+
}
58+
59+
let sortedHistory = Array.isArray(priceHistory)
60+
? [...priceHistory]
61+
.filter(h => h && h.changedAt && h.price !== undefined)
62+
.sort((a, b) => new Date(a.changedAt).getTime() - new Date(b.changedAt).getTime())
63+
: [];
64+
65+
if (sortedHistory.length > 0) {
66+
const last = sortedHistory[sortedHistory.length - 1];
67+
const lastDate = new Date(last.changedAt);
68+
const today = new Date();
69+
lastDate.setHours(0,0,0,0);
70+
today.setHours(0,0,0,0);
71+
72+
if (lastDate.getTime() < today.getTime()) {
73+
sortedHistory = [
74+
...sortedHistory,
75+
{
76+
...last,
77+
id: last.id + '-virtual',
78+
changedAt: today.toISOString(),
79+
}
80+
];
81+
}
82+
}
83+
84+
const chartData = {
85+
datasets: [
86+
{
87+
label: 'Price',
88+
data: sortedHistory.map(h => ({
89+
x: h.changedAt,
90+
y: h.discountedPrice !== undefined && h.discountedPrice !== null
91+
? h.discountedPrice
92+
: h.price
93+
})),
94+
fill: false,
95+
borderColor: '#c8ff00',
96+
backgroundColor: '#c8ff00',
97+
tension: 0.2,
98+
stepped: 'before' as const,
99+
},
100+
],
101+
};
102+
103+
const chartOptions: ChartOptions<'line'> = {
104+
responsive: true,
105+
maintainAspectRatio: false,
106+
plugins: {
107+
legend: { display: false },
108+
tooltip: {
109+
enabled: true,
110+
callbacks: {
111+
label: function (context) {
112+
const yValue = (context.raw as { x: any; y: number }).y;
113+
return `$${formatPrice(yValue)}`;
114+
},
115+
},
116+
},
117+
},
118+
scales: {
119+
x: {
120+
type: 'time',
121+
time: {
122+
unit: 'day',
123+
tooltipFormat: 'MM/dd/yy hh:mm:ss a',
124+
displayFormats: {
125+
day: 'MM/dd/yy',
126+
},
127+
},
128+
title: { display: false },
129+
grid: { display: false },
130+
border: { display: false },
131+
ticks: { color: "#FFF", maxTicksLimit: 4}
132+
},
133+
y: {
134+
title: { display: true, text: 'Price', color: "#FFF" },
135+
grid: { display: false },
136+
border: { display: false },
137+
ticks: {
138+
callback: function(value) {
139+
return `${formatPrice(Number(value))}`;
140+
},
141+
color: "#FFF"
142+
}
143+
},
144+
},
145+
};
146+
36147
return (
37148
<div className="item-card">
38149
<img src={itemImageUrl} alt={itemName}/>
@@ -42,18 +153,60 @@ const ItemCard: React.FC<ItemCardProps> = ({
42153
<div className="item-category">{category}</div>
43154
<div className="item-store">{storeName}</div>
44155
<div className="item-price">
45-
{isDiscount && discountedPrice !== undefined ? (
46-
<div>
47-
<p>${discountedPrice}</p>
48-
<s>${itemPrice}</s>
49-
</div>
50-
) : (
51-
<p>${itemPrice}</p>
52-
)}
156+
{isDiscount && discountedPrice !== undefined ? (
157+
<div>
158+
<p>${formatPrice(discountedPrice)}</p>
159+
<s>${formatPrice(itemPrice)}</s>
160+
</div>
161+
) : (
162+
<p>${formatPrice(itemPrice)}</p>
163+
)}
53164
</div>
54165
<button className="button edit-button" onClick={handleEditClick}>
55166
<FontAwesomeIcon icon={faPencilAlt} /> Edit
56167
</button>
168+
169+
{Array.isArray(priceHistory) && priceHistory.length > 0 && (
170+
<div className="price-history">
171+
<h4>Price History</h4>
172+
{sortedHistory.length > 0 && (
173+
<div className="price-history-graph">
174+
<Line data={chartData} options={chartOptions} />
175+
</div>
176+
)}
177+
<table className="price-history-table">
178+
<thead>
179+
<tr>
180+
<th>Date</th>
181+
<th>Price</th>
182+
</tr>
183+
</thead>
184+
<tbody>
185+
{[...priceHistory]
186+
.sort((a, b) => new Date(a.changedAt).getTime() - new Date(b.changedAt).getTime())
187+
.map((h) => (
188+
<tr key={h.id}>
189+
<td>
190+
{new Date(h.changedAt).toLocaleDateString(undefined, {
191+
year: '2-digit',
192+
month: '2-digit',
193+
day: '2-digit'
194+
})}
195+
</td>
196+
<td>
197+
{h.discountedPrice !== undefined && h.discountedPrice !== null
198+
? (
199+
<span>${formatPrice(h.discountedPrice)}</span>
200+
) : (
201+
<span>${formatPrice(h.price)}</span>
202+
)}
203+
</td>
204+
</tr>
205+
))}
206+
</tbody>
207+
</table>
208+
</div>
209+
)}
57210
</div>
58211
)
59212
}

src/index.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,20 @@ h1 {
628628
margin-left: auto;
629629
}
630630

631+
.price-history h4 {
632+
margin-top: 0;
633+
}
634+
635+
.price-history-graph {
636+
min-width: 100%;
637+
margin-bottom: 1rem;
638+
}
639+
640+
.price-history-table td {
641+
padding: 0.25rem 0.75rem;
642+
}
643+
644+
631645
@media (max-width: 1059.98px) { /* smaller desktop layout */
632646
.nav-bar {
633647
display: grid;

src/pages/CreateNewItem.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,21 @@ const CreateNewItem: React.FC = () => {
125125
}
126126

127127
try {
128-
await client.models.Item.create(sanitizedItemInputs);
128+
const result = await client.models.Item.create(sanitizedItemInputs);
129+
if (!result?.data) {
130+
throw new Error("no data returned on create new item");
131+
}
132+
const newItem = result.data;
133+
await client.models.PriceHistory.create({
134+
itemId: newItem.id,
135+
itemName: sanitizedItemInputs.itemName,
136+
price: sanitizedItemInputs.itemPrice,
137+
discountedPrice: sanitizedItemInputs.isDiscount
138+
? sanitizedItemInputs.discountedPrice
139+
: undefined,
140+
changedAt: new Date().toISOString(),
141+
});
142+
129143
const successMessage = `Item: ${sanitizedItemInputs.itemName} created successfully for $${sanitizedItemInputs.itemPrice} per ${sanitizedItemInputs.units}`;
130144
Swal.fire({
131145
title: 'New Item Successfully Created',

0 commit comments

Comments
 (0)