Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 42 additions & 4 deletions src/app/admin/components/UserAdmin/UserAdminCreditGrant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export function UserAdminCreditGrant({
const [customDescription, setCustomDescription] = useState<string>('');
const [expirationDate, setExpirationDate] = useState<string>('');
const [expiryHours, setExpiryHours] = useState<string>('');
const [neverExpire, setNeverExpire] = useState(false);

// API state
const [isGrantingCredit, setIsGrantingCredit] = useState(false);
Expand All @@ -55,8 +56,18 @@ export function UserAdminCreditGrant({
);
const expectNegative = selectedCreditCategory?.expect_negative_amount ?? false;

// Check if expiration is provided (either via date, hours, or category default)
const hasExpirationFromCategory =
selectedCreditCategory?.expiry_hours != null ||
selectedCreditCategory?.credit_expiry_date != null;
const hasExpirationFromForm = expirationDate.trim() !== '' || Number(expiryHours) > 0;
const hasExpiration = hasExpirationFromCategory || hasExpirationFromForm || neverExpire;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: "Never expire" doesn’t override category defaults

src/app/admin/components/UserAdmin/UserAdminCreditGrant.tsx:64 treats neverExpire as satisfying the "must have an expiration" validation, but the request payload sent in UserAdminCreditGrant.handleGrantCredit() doesn’t include any flag/override to tell the server to ignore selectedCreditCategory defaults. If the selected category has expiry_hours/credit_expiry_date, checking "Never expire" will still grant expiring credits (contradicting the UI text).

Consider either (a) sending an explicit override (e.g. never_expire: true or expiry_hours: null / credit_expiry_date: null with server support), or (b) hiding/disabled the checkbox when the category already has an expiry.

Comment thread
olearycrew marked this conversation as resolved.

// Form validation - credit category required; description required for negative amount categories
const isFormValid = selectedCredit && (!expectNegative || customDescription.trim().length > 0);
// For non-negative amounts, expiration is required unless "Never expire" is checked
const isExpirationValid = expectNegative || hasExpiration;
const isFormValid =
selectedCredit && (!expectNegative || customDescription.trim().length > 0) && isExpirationValid;

const handleCreditTypeChange = (value: string) => {
setSelectedCredit(value);
Expand All @@ -65,6 +76,7 @@ export function UserAdminCreditGrant({
setCustomDescription('');
setExpirationDate('');
setExpiryHours('');
setNeverExpire(false);
};

const handleGrantCredit = async () => {
Expand Down Expand Up @@ -109,6 +121,7 @@ export function UserAdminCreditGrant({
setCustomDescription('');
setExpirationDate('');
setExpiryHours('');
setNeverExpire(false);
await queryClient.invalidateQueries({ queryKey: ['admin-user-credit-transactions', id] });
} else {
setCreditMessage({
Expand Down Expand Up @@ -220,7 +233,7 @@ export function UserAdminCreditGrant({
<>
<div>
<Label className="text-sm font-medium" htmlFor="expiry-hours">
Expiry Hours
Expiry Hours{!hasExpirationFromCategory && !neverExpire ? ' *' : ''}
</Label>
<Input
type="number"
Expand All @@ -230,12 +243,12 @@ export function UserAdminCreditGrant({
min="0"
step="0.01"
id="expiry-hours"
disabled={!selectedCredit}
disabled={!selectedCredit || neverExpire}
/>
</div>
<div>
<Label className="text-sm font-medium" htmlFor="date">
Expiration Date
Expiration Date{!hasExpirationFromCategory && !neverExpire ? ' *' : ''}
Comment thread
olearycrew marked this conversation as resolved.
</Label>
<Input
type="date"
Expand All @@ -244,12 +257,37 @@ export function UserAdminCreditGrant({
}
onChange={e => setExpirationDate(e.target.value)}
id="date"
disabled={!selectedCredit || neverExpire}
/>
</div>
<div className="flex items-end">
<Label className="flex items-center gap-2 pb-2 text-sm font-medium">
<input
type="checkbox"
checked={neverExpire}
onChange={e => {
setNeverExpire(e.target.checked);
if (e.target.checked) {
setExpirationDate('');
setExpiryHours('');
}
}}
disabled={!selectedCredit}
/>
Never expire
</Label>
</div>
</>
)}
</div>

{!expectNegative && selectedCredit && !isExpirationValid && (
<div className="rounded-md border border-yellow-200 bg-yellow-50 p-3 text-sm text-yellow-800">
Please specify an expiration date or expiry hours, or check &quot;Never expire&quot; to
grant credits without expiration.
</div>
)}

<div className="flex flex-row flex-wrap gap-4">
<div className="grow">
<Label className="text-sm font-medium" htmlFor="description">
Expand Down