Skip to content

Commit f7c2f08

Browse files
committed
split across more files and supposedly add mobile support
1 parent bcb396d commit f7c2f08

16 files changed

Lines changed: 1746 additions & 1162 deletions

src/components/ApprovalSection.jsx

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { CheckCircle, AlertCircle } from 'lucide-react';
2+
import { calculateTotalCost } from '../utils/purchaseHelpers';
3+
4+
export default function ApprovalSection({
5+
purchase,
6+
user,
7+
validation,
8+
approvalLoading,
9+
onApprove,
10+
onWithdraw,
11+
canApproveRequest,
12+
canOverwriteApproval,
13+
isApproverValid,
14+
inDisallowedState
15+
}) {
16+
const totalCost = parseFloat(calculateTotalCost(purchase).replace(/[^0-9.-]+/g, '')) || 0;
17+
18+
const getUserApprovalPermissions = () => {
19+
const userName = user.name;
20+
const permissions = {
21+
canStudentApprove: false,
22+
studentApprovalLimit: 0,
23+
canMentorApprove: false,
24+
mentorApprovalLimit: 0
25+
};
26+
27+
if (validation['Presidents']?.includes(userName)) {
28+
permissions.canStudentApprove = true;
29+
permissions.studentApprovalLimit = Infinity;
30+
} else if (validation['Leadership']?.includes(userName)) {
31+
permissions.canStudentApprove = true;
32+
permissions.studentApprovalLimit = 500;
33+
}
34+
35+
if (validation['Mentors']?.includes(userName)) {
36+
permissions.canMentorApprove = true;
37+
permissions.mentorApprovalLimit = 500;
38+
} else if (validation['Directors']?.includes(userName)) {
39+
permissions.canMentorApprove = true;
40+
permissions.mentorApprovalLimit = Infinity;
41+
}
42+
43+
return permissions;
44+
};
45+
46+
return (
47+
<div className="border-t pt-4 md:pt-6">
48+
<h3 className="text-base md:text-lg font-semibold text-gray-800 mb-3 md:mb-4">Approvals</h3>
49+
50+
{/* Warning if over $2000 */}
51+
{totalCost > 2000 && (
52+
<div className="bg-red-50 border border-red-200 rounded-lg p-3 md:p-4 mb-3 md:mb-4 flex items-start animate-slideDown">
53+
<AlertCircle className="w-5 h-5 text-red-600 mr-3 mt-0.5 flex-shrink-0" />
54+
<div>
55+
<p className="font-semibold text-red-800 text-sm md:text-base">Cannot Approve</p>
56+
<p className="text-xs md:text-sm text-red-700">
57+
Requests over $2,000 cannot be approved through this system.
58+
</p>
59+
</div>
60+
</div>
61+
)}
62+
63+
{/* No Permissions */}
64+
{!getUserApprovalPermissions().canStudentApprove && !getUserApprovalPermissions().canMentorApprove && (
65+
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 md:p-4 mb-3 md:mb-4 flex items-start">
66+
<AlertCircle className="w-5 h-5 text-yellow-600 mr-3 mt-0.5 flex-shrink-0" />
67+
<div>
68+
<p className="font-semibold text-yellow-800 text-sm md:text-base">No Approval Permissions</p>
69+
<p className="text-xs md:text-sm text-yellow-700">
70+
You are not authorized to approve purchase requests.
71+
</p>
72+
</div>
73+
</div>
74+
)}
75+
76+
{/* Student Approver */}
77+
<div className="bg-gray-50 p-3 md:p-4 rounded-lg mb-3">
78+
<p className="text-xs md:text-sm text-gray-500 mb-2 font-medium">Student Approver</p>
79+
{purchase['S Approver'] ? (
80+
<div className="space-y-2">
81+
<div className="flex items-center gap-2">
82+
{isApproverValid(purchase['S Approver'], 'student', totalCost) ? (
83+
<CheckCircle className="w-5 h-5 text-green-600 flex-shrink-0" />
84+
) : (
85+
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0" />
86+
)}
87+
<div className="flex-1 min-w-0">
88+
<p className="font-semibold text-gray-800 text-sm md:text-base truncate">
89+
{purchase['S Approver']}
90+
</p>
91+
{!isApproverValid(purchase['S Approver'], 'student', totalCost) && (
92+
<p className="text-xs text-red-600">Invalid approver for this amount</p>
93+
)}
94+
</div>
95+
</div>
96+
<div className="flex flex-wrap gap-2">
97+
{purchase['S Approver'] === user.name && !inDisallowedState(purchase) && (
98+
<button
99+
onClick={() => onWithdraw('student')}
100+
disabled={approvalLoading}
101+
className="flex-1 sm:flex-none bg-red-600 hover:bg-red-700 disabled:bg-gray-400 text-white font-semibold py-2 px-4 rounded-lg transition duration-200 text-sm transform active:scale-95"
102+
>
103+
{approvalLoading ? 'Withdrawing...' : 'Withdraw'}
104+
</button>
105+
)}
106+
{canOverwriteApproval(purchase, 'student') && (
107+
<button
108+
onClick={() => onApprove('student', true)}
109+
disabled={approvalLoading}
110+
className="flex-1 sm:flex-none bg-orange-600 hover:bg-orange-700 disabled:bg-gray-400 text-white font-semibold py-2 px-4 rounded-lg transition duration-200 text-sm transform active:scale-95"
111+
>
112+
{approvalLoading ? 'Overwriting...' : 'Overwrite'}
113+
</button>
114+
)}
115+
</div>
116+
</div>
117+
) : (
118+
<div className="flex items-center justify-between gap-3">
119+
<p className="text-gray-500 italic text-sm">Not yet approved</p>
120+
{(() => {
121+
const approval = canApproveRequest(purchase, 'student');
122+
return approval.canApprove ? (
123+
<button
124+
onClick={() => onApprove('student', false)}
125+
disabled={approvalLoading}
126+
className="bg-green-600 hover:bg-green-700 disabled:bg-gray-400 text-white font-semibold py-2 px-4 rounded-lg transition duration-200 text-sm whitespace-nowrap transform active:scale-95"
127+
>
128+
{approvalLoading ? 'Approving...' : 'Approve'}
129+
</button>
130+
) : null;
131+
})()}
132+
</div>
133+
)}
134+
</div>
135+
136+
{/* Mentor Approver */}
137+
<div className="bg-gray-50 p-3 md:p-4 rounded-lg">
138+
<p className="text-xs md:text-sm text-gray-500 mb-2 font-medium">Mentor Approver</p>
139+
{purchase['M Approver'] ? (
140+
<div className="space-y-2">
141+
<div className="flex items-center gap-2">
142+
{isApproverValid(purchase['M Approver'], 'mentor', totalCost) ? (
143+
<CheckCircle className="w-5 h-5 text-green-600 flex-shrink-0" />
144+
) : (
145+
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0" />
146+
)}
147+
<div className="flex-1 min-w-0">
148+
<p className="font-semibold text-gray-800 text-sm md:text-base truncate">
149+
{purchase['M Approver']}
150+
</p>
151+
{!isApproverValid(purchase['M Approver'], 'mentor', totalCost) && (
152+
<p className="text-xs text-red-600">Invalid approver for this amount</p>
153+
)}
154+
</div>
155+
</div>
156+
<div className="flex flex-wrap gap-2">
157+
{purchase['M Approver'] === user.name && !inDisallowedState(purchase) && (
158+
<button
159+
onClick={() => onWithdraw('mentor')}
160+
disabled={approvalLoading}
161+
className="flex-1 sm:flex-none bg-red-600 hover:bg-red-700 disabled:bg-gray-400 text-white font-semibold py-2 px-4 rounded-lg transition duration-200 text-sm transform active:scale-95"
162+
>
163+
{approvalLoading ? 'Withdrawing...' : 'Withdraw'}
164+
</button>
165+
)}
166+
{canOverwriteApproval(purchase, 'mentor') && (
167+
<button
168+
onClick={() => onApprove('mentor', true)}
169+
disabled={approvalLoading}
170+
className="flex-1 sm:flex-none bg-orange-600 hover:bg-orange-700 disabled:bg-gray-400 text-white font-semibold py-2 px-4 rounded-lg transition duration-200 text-sm transform active:scale-95"
171+
>
172+
{approvalLoading ? 'Overwriting...' : 'Overwrite'}
173+
</button>
174+
)}
175+
</div>
176+
</div>
177+
) : (
178+
<div className="flex items-center justify-between gap-3">
179+
<p className="text-gray-500 italic text-sm">Not yet approved</p>
180+
{(() => {
181+
const approval = canApproveRequest(purchase, 'mentor');
182+
return approval.canApprove ? (
183+
<button
184+
onClick={() => onApprove('mentor', false)}
185+
disabled={approvalLoading}
186+
className="bg-indigo-600 hover:bg-indigo-700 disabled:bg-gray-400 text-white font-semibold py-2 px-4 rounded-lg transition duration-200 text-sm whitespace-nowrap transform active:scale-95"
187+
>
188+
{approvalLoading ? 'Approving...' : 'Approve'}
189+
</button>
190+
) : null;
191+
})()}
192+
</div>
193+
)}
194+
</div>
195+
</div>
196+
);
197+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Loader } from 'lucide-react';
2+
3+
export default function EditActionsFooter({ savingLoading, onCancel, onSave }) {
4+
return (
5+
<div className="sticky bottom-0 border-t bg-white p-4 md:p-6 shadow-lg z-20">
6+
<div className="flex flex-col sm:flex-row gap-3 justify-end">
7+
<button
8+
onClick={onCancel}
9+
className="w-full sm:w-auto bg-gray-300 hover:bg-gray-400 text-gray-800 font-semibold py-3 px-6 rounded-lg transition duration-200 transform active:scale-95"
10+
>
11+
Cancel
12+
</button>
13+
<button
14+
onClick={onSave}
15+
disabled={savingLoading}
16+
className="w-full sm:w-auto bg-green-600 hover:bg-green-700 disabled:bg-gray-400 text-white font-semibold py-3 px-6 rounded-lg transition duration-200 transform active:scale-95 flex items-center justify-center"
17+
>
18+
{savingLoading ? (
19+
<>
20+
<Loader className="animate-spin w-5 h-5 mr-2" />
21+
Saving...
22+
</>
23+
) : (
24+
'Save Changes'
25+
)}
26+
</button>
27+
</div>
28+
</div>
29+
);
30+
}

src/components/ModalHeader.jsx

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { X, Pencil, Trash2, Loader, XCircle } from 'lucide-react';
2+
import StateBadge from './StateBadge';
3+
4+
export default function ModalHeader({
5+
purchase,
6+
isEditing,
7+
savingLoading,
8+
canDelete,
9+
canEdit,
10+
onClose,
11+
onDelete,
12+
onToggleEdit,
13+
onCancelEdit
14+
}) {
15+
return (
16+
<div className="sticky top-0 z-30 bg-gradient-to-r from-red-700 to-orange-800 text-white shadow-lg">
17+
{/* Drag Handle (Mobile Only) */}
18+
<div className="md:hidden flex justify-center pt-2 pb-1">
19+
<div className="w-12 h-1 bg-white/60 rounded-full"></div>
20+
</div>
21+
22+
<div className="p-4 md:p-6">
23+
<div className="flex items-start justify-between gap-3">
24+
<div className="flex-1 min-w-0">
25+
<h2 className="text-lg md:text-2xl font-bold mb-1 line-clamp-2">
26+
{purchase['Item Description']}
27+
</h2>
28+
<div className="flex items-center gap-2 flex-wrap">
29+
<p className="text-blue-100 text-xs md:text-sm">
30+
ID: {purchase['Request ID']}
31+
</p>
32+
{purchase['State'] && <StateBadge state={purchase['State']} />}
33+
</div>
34+
</div>
35+
36+
<div className="flex items-center gap-1 md:gap-2 flex-shrink-0">
37+
{canDelete && (
38+
<button
39+
onClick={onDelete}
40+
className="p-2 rounded-full bg-red-500 hover:bg-red-600 text-white flex items-center justify-center transition disabled:opacity-50 transform active:scale-90"
41+
disabled={savingLoading}
42+
title="Delete purchase"
43+
>
44+
{savingLoading ? (
45+
<Loader className="animate-spin w-5 h-5" />
46+
) : (
47+
<Trash2 className="w-4 h-4 md:w-5 md:h-5" />
48+
)}
49+
</button>
50+
)}
51+
52+
{canEdit && (
53+
<>
54+
{!isEditing ? (
55+
<button
56+
onClick={onToggleEdit}
57+
disabled={purchase['S Approver'] && purchase['M Approver']}
58+
className={`p-2 rounded-full transition duration-200 transform active:scale-90 ${
59+
purchase['S Approver'] && purchase['M Approver']
60+
? 'bg-white/10 text-white/50 cursor-not-allowed'
61+
: 'bg-yellow-500 hover:bg-yellow-600 text-white'
62+
}`}
63+
title={
64+
purchase['S Approver'] && purchase['M Approver']
65+
? 'Cannot edit after approval'
66+
: 'Edit item'
67+
}
68+
>
69+
<Pencil className="w-4 h-4 md:w-5 md:h-5" />
70+
</button>
71+
) : (
72+
<button
73+
onClick={onCancelEdit}
74+
className="p-2 rounded-full bg-red-500 hover:bg-red-600 text-white transition duration-200 transform active:scale-90"
75+
title="Cancel edit mode"
76+
>
77+
<XCircle className="w-4 h-4 md:w-5 md:h-5" />
78+
</button>
79+
)}
80+
</>
81+
)}
82+
83+
<button
84+
onClick={onClose}
85+
className="text-white hover:bg-white/20 rounded-lg p-2 transition transform active:scale-90"
86+
title="Close"
87+
>
88+
<X className="w-5 h-5 md:w-6 md:h-6" />
89+
</button>
90+
</div>
91+
</div>
92+
</div>
93+
</div>
94+
);
95+
}

0 commit comments

Comments
 (0)