Skip to content

Commit 7d4cb32

Browse files
committed
groupings work
1 parent 4929592 commit 7d4cb32

9 files changed

Lines changed: 1257 additions & 56 deletions

File tree

src/components/PurchaseDetailModal.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import EditActionsFooter from './EditActionsFooter';
1010
import { useModalDrag } from '../hooks/useModalDrag';
1111
import { useApprovalLogic } from '../hooks/useApprovalLogic';
1212

13-
export default function PurchaseDetailModal({ purchase, user, validation, onClose, onUpdate }) {
13+
export default function PurchaseDetailModal({ purchase, user, validation, onClose, onUpdate, existingPurchases = [] }) {
1414
const [isEditing, setIsEditing] = useState(false);
1515
const [editedPurchase, setEditedPurchase] = useState({});
1616
const [approvalLoading, setApprovalLoading] = useState(false);
@@ -247,6 +247,7 @@ export default function PurchaseDetailModal({ purchase, user, validation, onClos
247247
editedPurchase={editedPurchase}
248248
originalTier={originalTier}
249249
onEditChange={handleEditChange}
250+
existingPurchases={existingPurchases}
250251
/>
251252

252253
<ApprovalSection

src/components/PurchaseInfoSection.jsx

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,27 @@
11
import { ExternalLink, AlertTriangle } from 'lucide-react';
22
import { formatDate, formatCurrency, parseCurrency, calculateTotalCost, getRequestTier } from '../utils/purchaseHelpers';
33
import { CATEGORIES } from '../utils/purchaseHelpers';
4+
import GroupNameAutocomplete from './groups/GroupNameAutoComplete.jsx';
45

56
export default function PurchaseInfoSection({
67
purchase,
78
isEditing,
89
editedPurchase,
910
originalTier,
10-
onEditChange
11+
onEditChange,
12+
existingPurchases = []
1113
}) {
14+
// Extract unique group names from existing purchases
15+
const existingGroups = [...new Set(
16+
existingPurchases
17+
.map(p => p['Group Name'])
18+
.filter(name => name && name.trim() !== '')
19+
)].sort();
20+
1221
return (
1322
<>
14-
{/* Basic Info Grid */}
23+
{/* Category and Group Name Grid */}
1524
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 md:gap-4">
16-
<div className="bg-gray-50 p-3 md:p-4 rounded-lg">
17-
<p className="text-xs md:text-sm text-gray-500 mb-1">Date Requested</p>
18-
<p className="font-semibold text-sm md:text-base text-gray-800">
19-
{formatDate(purchase['Date Requested'])}
20-
</p>
21-
</div>
22-
<div className="bg-gray-50 p-3 md:p-4 rounded-lg">
23-
<p className="text-xs md:text-sm text-gray-500 mb-1">Requester</p>
24-
<p className="font-semibold text-sm md:text-base text-gray-800 truncate">
25-
{purchase['Requester'] || 'N/A'}
26-
</p>
27-
</div>
2825
<div className="bg-gray-50 p-3 md:p-4 rounded-lg">
2926
<p className="text-xs md:text-sm text-gray-500 mb-1">Category</p>
3027
{isEditing ? (
@@ -45,6 +42,41 @@ export default function PurchaseInfoSection({
4542
</p>
4643
)}
4744
</div>
45+
<div className="bg-gray-50 p-3 md:p-4 rounded-lg">
46+
<p className="text-xs md:text-sm text-gray-500 mb-1">Group Name</p>
47+
{isEditing ? (
48+
<GroupNameAutocomplete
49+
value={editedPurchase['Group Name'] || ''}
50+
onChange={(value) => onEditChange('Group Name', value)}
51+
existingGroups={existingGroups}
52+
placeholder="Optional"
53+
/>
54+
) : (
55+
<p className="font-semibold text-sm md:text-base text-gray-800">
56+
{purchase['Group Name'] || 'None'}
57+
</p>
58+
)}
59+
</div>
60+
</div>
61+
62+
{/* Date Requested and Requester Grid */}
63+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 md:gap-4">
64+
<div className="bg-gray-50 p-3 md:p-4 rounded-lg">
65+
<p className="text-xs md:text-sm text-gray-500 mb-1">Date Requested</p>
66+
<p className="font-semibold text-sm md:text-base text-gray-800">
67+
{formatDate(purchase['Date Requested'])}
68+
</p>
69+
</div>
70+
<div className="bg-gray-50 p-3 md:p-4 rounded-lg">
71+
<p className="text-xs md:text-sm text-gray-500 mb-1">Requester</p>
72+
<p className="font-semibold text-sm md:text-base text-gray-800 truncate">
73+
{purchase['Requester'] || 'N/A'}
74+
</p>
75+
</div>
76+
</div>
77+
78+
{/* Quantity Grid */}
79+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 md:gap-4">
4880
<div className="bg-gray-50 p-3 md:p-4 rounded-lg">
4981
<p className="text-xs md:text-sm text-gray-500 mb-1">Quantity</p>
5082
{isEditing ? (

src/components/RequestForm.jsx

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { X, PlusCircle } from 'lucide-react';
33
import { createPurchase } from '../utils/googleSheets.js';
44
import { getRefreshedAccessToken } from '../utils/googleAuth.js';
55
import { useAlert } from './AlertContext';
6+
import GroupNameAutocomplete from './groups/GroupNameAutoComplete.jsx';
67

78
const CATEGORIES = [
89
'Robot',
@@ -15,19 +16,37 @@ const CATEGORIES = [
1516
'Other'
1617
];
1718

18-
export default function RequestForm({ user, onClose, onCreated, presetFields = {} }) {
19+
export default function RequestForm({ user, onClose, onCreated, presetFields = {}, existingPurchases = [] }) {
1920
const { showError } = useAlert();
21+
22+
// Get today's date in local timezone
23+
const getLocalDate = () => {
24+
const today = new Date();
25+
const localDate = new Date(today.getTime() - (today.getTimezoneOffset() * 60000))
26+
.toISOString()
27+
.split('T')[0];
28+
return localDate;
29+
};
30+
31+
// Extract unique group names from existing purchases
32+
const existingGroups = [...new Set(
33+
existingPurchases
34+
.map(p => p['Group Name'])
35+
.filter(name => name && name.trim() !== '')
36+
)].sort();
37+
2038
const [newRequest, setNewRequest] = useState({
2139
'Request ID': '',
2240
'Item Description': '',
2341
'Item Link': '',
2442
'Category': '',
43+
'Group Name': '',
2544
'Quantity': '',
2645
'Unit Price': '',
2746
'Shipping': '',
2847
'Package Size': '',
2948
'Comments': '',
30-
'Date Requested': new Date().toISOString().split('T')[0],
49+
'Date Requested': getLocalDate(),
3150
'Requester': 'N/A',
3251
'State': 'Pending Approval',
3352
...presetFields
@@ -51,7 +70,7 @@ export default function RequestForm({ user, onClose, onCreated, presetFields = {
5170

5271
const handleClose = () => {
5372
setIsClosing(true);
54-
setTimeout(() => onClose(), 300); // Match animation duration
73+
setTimeout(() => onClose(), 300);
5574
};
5675

5776
const handleCreateRequest = async () => {
@@ -77,7 +96,6 @@ export default function RequestForm({ user, onClose, onCreated, presetFields = {
7796
}`}
7897
onClick={handleClose}
7998
>
80-
{/* Mobile: Full screen, Desktop: Modal */}
8199
<div
82100
className={`
83101
bg-white shadow-2xl w-full h-full overflow-y-auto
@@ -144,23 +162,37 @@ export default function RequestForm({ user, onClose, onCreated, presetFields = {
144162
/>
145163
</div>
146164

147-
{/* Category */}
148-
<div>
149-
<label className="block text-sm font-semibold text-gray-700 mb-2">
150-
Category <span className="text-red-500">*</span>
151-
</label>
152-
<select
153-
value={newRequest['Category']}
154-
onChange={(e) =>
155-
setNewRequest(prev => ({ ...prev, 'Category': e.target.value }))
156-
}
157-
className="w-full border border-gray-300 rounded-lg px-4 py-3 focus:ring-2 focus:ring-green-500 focus:border-green-500 text-gray-700 transition text-base"
158-
>
159-
<option value="">Select a category</option>
160-
{CATEGORIES.map(cat => (
161-
<option key={cat} value={cat}>{cat}</option>
162-
))}
163-
</select>
165+
{/* Category and Group Name */}
166+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
167+
<div>
168+
<label className="block text-sm font-semibold text-gray-700 mb-2">
169+
Category <span className="text-red-500">*</span>
170+
</label>
171+
<select
172+
value={newRequest['Category']}
173+
onChange={(e) =>
174+
setNewRequest(prev => ({ ...prev, 'Category': e.target.value }))
175+
}
176+
className="w-full border border-gray-300 rounded-lg px-4 py-3 focus:ring-2 focus:ring-green-500 focus:border-green-500 text-gray-700 transition text-base"
177+
>
178+
<option value="">Select a category</option>
179+
{CATEGORIES.map(cat => (
180+
<option key={cat} value={cat}>{cat}</option>
181+
))}
182+
</select>
183+
</div>
184+
185+
<div>
186+
<label className="block text-sm font-semibold text-gray-700 mb-2">
187+
Group Name <span className="text-gray-400 text-xs font-normal">(Optional)</span>
188+
</label>
189+
<GroupNameAutocomplete
190+
value={newRequest['Group Name']}
191+
onChange={(value) => setNewRequest(prev => ({ ...prev, 'Group Name': value }))}
192+
existingGroups={existingGroups}
193+
placeholder="e.g., Robot Build 2025"
194+
/>
195+
</div>
164196
</div>
165197

166198
{/* Quantity and Unit Price */}

src/components/dashboard/DashboardHeader.jsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { LogOut, RefreshCw, Plus, Menu, X, CheckSquare, Square } from 'lucide-react';
1+
import { LogOut, RefreshCw, Plus, Menu, X, CheckSquare, Square, List, LayoutGrid } from 'lucide-react';
22
import { useState, useEffect } from 'react';
33

44
export default function DashboardHeader({
@@ -9,7 +9,9 @@ export default function DashboardHeader({
99
refreshing,
1010
selectionMode,
1111
onToggleSelectionMode,
12-
selectedCount
12+
selectedCount,
13+
viewMode,
14+
onToggleViewMode
1315
}) {
1416
const [showMenu, setShowMenu] = useState(false);
1517
const [isAnimating, setIsAnimating] = useState(false);
@@ -64,6 +66,17 @@ export default function DashboardHeader({
6466
}`}
6567
>
6668
<div className="px-4 pb-4 space-y-2 border-t border-white/20 pt-4">
69+
<button
70+
onClick={() => {
71+
onToggleViewMode();
72+
handleCloseMenu();
73+
}}
74+
className="w-full flex items-center justify-center gap-2 bg-indigo-500 hover:bg-indigo-600 text-white py-3 px-4 rounded-lg font-semibold transition shadow-lg"
75+
>
76+
{viewMode === 'list' ? <LayoutGrid size={20} /> : <List size={20} />}
77+
<span>{viewMode === 'list' ? 'Group View' : 'List View'}</span>
78+
</button>
79+
6780
<button
6881
onClick={() => {
6982
onToggleSelectionMode();
@@ -127,10 +140,19 @@ export default function DashboardHeader({
127140
{selectionMode ? (
128141
<p className="text-blue-100">{selectedCount} item{selectedCount !== 1 ? 's' : ''} selected</p>
129142
) : (
130-
<p className="text-blue-100">Purchase Requests</p>
143+
<p className="text-blue-100">{viewMode === 'list' ? 'List View' : 'Group View'}</p>
131144
)}
132145
</div>
133146

147+
<button
148+
onClick={onToggleViewMode}
149+
className="bg-indigo-500 hover:bg-indigo-600 text-white font-semibold py-2 px-4 rounded-lg transition duration-200 flex items-center gap-2"
150+
title={viewMode === 'list' ? "Switch to Group View" : "Switch to List View"}
151+
>
152+
{viewMode === 'list' ? <LayoutGrid className="w-5 h-5" /> : <List className="w-5 h-5" />}
153+
{viewMode === 'list' ? 'Group View' : 'List View'}
154+
</button>
155+
134156
<button
135157
onClick={onToggleSelectionMode}
136158
className="bg-purple-500 hover:bg-purple-600 text-white font-semibold py-2 px-4 rounded-lg transition duration-200 flex items-center gap-2"

src/components/dashboard/PurchaseCard.jsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Calendar, User, Package, DollarSign, Truck, Box, ChevronRight, Check } from 'lucide-react';
1+
import { Calendar, User, Package, DollarSign, Truck, Box, ChevronRight, Check, Tag } from 'lucide-react';
22
import StateBadge from '../StateBadge';
33
import { formatDate, formatCurrency, calculateTotalCost, getAvailableStateTransitions, STATE_COLORS } from '../../utils/purchaseHelpers';
44

@@ -64,7 +64,15 @@ export default function PurchaseCard({
6464
<h3 className="text-base font-semibold text-gray-800 mb-1 line-clamp-2">
6565
{purchase['Item Description'] || 'No description'}
6666
</h3>
67-
{purchase['State'] && <StateBadge state={purchase['State']} />}
67+
<div className="flex items-center gap-2 flex-wrap">
68+
{purchase['State'] && <StateBadge state={purchase['State']} />}
69+
{purchase['Group Name'] && (
70+
<span className="inline-flex items-center gap-1 px-2 py-1 bg-purple-100 text-purple-800 text-xs font-semibold rounded-full">
71+
<Tag className="w-3 h-3" />
72+
{purchase['Group Name']}
73+
</span>
74+
)}
75+
</div>
6876
</div>
6977
</div>
7078
{!selectionMode && <ChevronRight className="w-5 h-5 text-gray-400 flex-shrink-0 mt-1" />}
@@ -225,6 +233,12 @@ export default function PurchaseCard({
225233
{purchase['Item Description'] || 'No description'}
226234
</h3>
227235
{purchase['State'] && <StateBadge state={purchase['State']} />}
236+
{purchase['Group Name'] && (
237+
<span className="inline-flex items-center gap-1 px-3 py-1 bg-purple-100 text-purple-800 text-xs font-semibold rounded-full">
238+
<Tag className="w-3 h-3" />
239+
{purchase['Group Name']}
240+
</span>
241+
)}
228242
</div>
229243

230244
<div className="grid grid-cols-4 gap-3 mt-3 text-sm">

0 commit comments

Comments
 (0)