Skip to content

Ezra/280 advanced table filters#290

Merged
ND68 merged 15 commits intomainfrom
280-advanced-table-filters
Mar 4, 2026
Merged

Ezra/280 advanced table filters#290
ND68 merged 15 commits intomainfrom
280-advanced-table-filters

Conversation

@ezraexe
Copy link
Contributor

@ezraexe ezraexe commented Feb 23, 2026

Description

Resolves ticket number: #280

Explain what your code changes:

Issue 1: Account Management

Relevant Files: AccountManagementPage/page.tsx
The Role filter was not working properly, so added a filterType: "enum" was added along with filterOptions being the prisma UserTypes

image

Issue 2: Distributions

Relevant Files: DistributionTable.tsx and distributionService.tsx

  • Added a filterType to "string" for partner name search and filter with "enum" for pending and status column
  • The underlying data is a boolean value, but users see "Pending" or "Approved" in the table. It would previously infer types, but now, we can set the to enum with "Pending" and "Approved" to let them see whats happening.
  • Change depending filter comparison in distributionService.tsx to "Pending" to match the data that the frontend is sending

Before

image

After

image

Issue 3: Shipments & Signoffs Tables

Relevant Files: ShipmentsTable.tsx, SignOffsTable.tsx, shippingStatusService.ts

  • Added filterType "string" to donorShippingNumber and changed the status column id from "status" to "value" to match what is in the prisma types
  • Added filterType "enum" with filterOptions with shippingStatusToText
  • The frontend changes are the same in ShipmentsTable and SignOffsTable as the code is relatively similar
  • shippingStatusService, added mapping logic to convert display names to Prisma enum values for the backend to handle
image image

Issue 4: Donor Offers Page(Unfinalized & Finalized)

Relevant Files: AdminDonorOffersScreen.tsx, donorOffer.types.ts, donorOfferService.ts

  • Replaced BaseTable and useFetch with AdvancedBaseTable + fetchFn / useCallback
  • Defined the columns with filter types
  • Added donor response deadline to the Admin Donor Offer page
  • Added server sided filtering logic for donor offers: mapped donorResponseDeadline to the response type, added custom filters for state, response deadline, and partner involved.
  • Added filtering for computed all partners responded field as well post query

Before

image

After

image

List the steps you took to test your code: I used seed.ts, added mock data manually using the Prisma DB, and added the csv data to the donor offers to filter.

Checklist

  • The ticket is mentioned above
  • The changes fulfill the success criteria of the ticket
  • The changes were self-reviewed
  • A review was requested

@ezraexe ezraexe requested a review from ND68 February 23, 2026 04:53
@ezraexe ezraexe linked an issue Feb 23, 2026 that may be closed by this pull request
@netlify
Copy link

netlify bot commented Feb 23, 2026

Deploy Preview for hope-for-haiti ready!

Name Link
🔨 Latest commit 63b3629
🔍 Latest deploy log https://app.netlify.com/projects/hope-for-haiti/deploys/69a898a8943a4d00089936fa
😎 Deploy Preview https://deploy-preview-290--hope-for-haiti.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 23, 2026

Greptile Summary

Implements advanced table filtering across multiple admin pages (Account Management, Distributions, Shipments, Sign-offs, Donor Offers) by adding filterType and filterOptions to column definitions and implementing server-side filter handling.

Key changes:

  • Migrated Donor Offers page from BaseTable to AdvancedBaseTable with server-side pagination and filtering
  • Added new columns: donor response deadline, partner involved, and all partners responded
  • Implemented custom filter logic for enum-to-boolean conversions (pending status), nested relations (partner names), and computed fields (all partners responded)
  • Added display name to Prisma enum mapping for shipping statuses

Critical issues found:

  • donorOfferService.ts: Removed total count variable causing pagination to return incorrect total (uses post-filter count instead of database count)
  • donorOfferService.ts: Filter field responseDeadline incorrectly maps to partnerResponseDeadline instead of donorResponseDeadline
  • donorOfferService.ts: Post-query filtering for allPartnersResponded after pagination returns incomplete results
  • shippingStatusService.ts: Filters ignored when isCompleted parameter is used, affecting shipments/signoffs tables

Confidence Score: 2/5

  • This PR has critical bugs in the donor offer service that will break pagination and filtering functionality
  • The PR contains 3 critical logical errors in donorOfferService.ts that will cause incorrect pagination totals, wrong filter field mappings, and incomplete filtered results. Additionally, shipment filters won't work properly when the isCompleted parameter is used. These are not edge cases - they will affect core table functionality.
  • src/services/donorOfferService.ts requires immediate fixes for pagination and filtering logic; src/services/shippingStatusService.ts needs filter integration for completed shipments

Important Files Changed

Filename Overview
src/screens/DonorOffersScreens/AdminDonorOffersScreen.tsx Migrated from BaseTable to AdvancedBaseTable with server-side filtering; added donor response deadline, partner involved, and all partners responded columns
src/services/donorOfferService.ts Added filtering for state, responseDeadline, partnerInvolved, and allPartnersResponded; contains critical bugs with pagination and filter field mapping
src/services/shippingStatusService.ts Added display name to enum value mapping for status filters; filters not applied when isCompleted parameter is used

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User applies filter on Donor Offers table] --> B[Frontend: AdminDonorOffersScreen]
    B --> C[fetchFn combines filters with activeTab state]
    C --> D[API: GET /api/donorOffers with filters]
    D --> E[donorOfferService.getAdminDonorOffers]
    E --> F{Filter type?}
    F -->|state enum| G[Map to Prisma DonorOfferState]
    F -->|responseDeadline date| H[❌ Maps to partnerResponseDeadline WRONG FIELD]
    F -->|partnerInvolved string| I[Query partnerVisibilities relation]
    F -->|allPartnersResponded enum| J[❌ Post-query filter AFTER pagination]
    G --> K[Build Prisma query with WHERE clause]
    H --> K
    I --> K
    K --> L[Apply pagination skip/take]
    L --> M[Execute db.donorOffer.findMany]
    M --> N[❌ total count removed - pagination broken]
    N --> O[Map to AdminDonorOffer objects]
    O --> J
    J --> P[Return filteredOffers with wrong total]
    P --> Q[Frontend displays incomplete/incorrect results]
Loading

Last reviewed commit: 4c4e79a

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

9 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 447 to 450
const [donorOffers] = await Promise.all([
db.donorOffer.findMany(query),
db.donorOffer.count({ where }),
]);
Copy link
Contributor

Choose a reason for hiding this comment

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

The total variable from db.donorOffer.count() is computed but never used (was removed on line 447). Later on line 482, the function returns total: filteredOffers.length, which breaks pagination because the total count doesn't account for the database-level filters - it only counts the post-filtering results. This causes incorrect pagination metadata.

Suggested change
const [donorOffers] = await Promise.all([
db.donorOffer.findMany(query),
db.donorOffer.count({ where }),
]);
const [donorOffers, total] = await Promise.all([
db.donorOffer.findMany(query),
db.donorOffer.count({ where }),
]);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@greptile review

Copy link
Contributor

Choose a reason for hiding this comment

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

You need to restore the total variable in the destructuring on line 447 and use it in the return statement instead of filteredOffers.length.

Suggested change
const [donorOffers] = await Promise.all([
db.donorOffer.findMany(query),
db.donorOffer.count({ where }),
]);
const [donorOffers, total] = await Promise.all([
db.donorOffer.findMany(query),
db.donorOffer.count({ where }),
]);

Then in your return statement (around line 482), make sure you're returning the database total instead of filteredOffers.length:

return { donorOffers: filteredOffers, total };

The current implementation breaks pagination because filteredOffers.length only reflects items after client-side filtering, not the total count from the database query. Pagination metadata needs the database count to work correctly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@greptile review

Copy link
Contributor

Choose a reason for hiding this comment

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

Looking at the current implementation, I can see the issue. The code computes total from db.donorOffer.count() on line 450 but then discards it on line 447. Later, the function returns filteredOffers.length instead of total, which breaks pagination.

However, there's a complication: you're doing post-query filtering for the "All Partners Responded" field. This means the total from the database won't match filteredOffers.length after client-side filtering.

You need to choose one approach:

Option 1 (Recommended): Move all filtering to the database level

  • Add allPartnersResponded as a computed field in Prisma or pre-calculate it
  • Remove client-side filtering
  • Use database total for pagination

Option 2: Keep client-side filtering but fix the count

  • You'll need to run the count query WITH the same post-filtering logic
  • This means fetching all records matching the base query, filtering them, then counting

Right now you're in a broken middle state where:

  1. Database returns N records with total = N
  2. Client filters down to M records (M < N)
  3. You return total: filteredOffers.length (M) but pagination still uses the original query

The fix depends on your filtering requirements. If you must keep client-side filtering for "All Partners Responded", you'll need to fetch all matching records to get an accurate count, which defeats the purpose of pagination.

Quick fix for now:

Suggested change
const [donorOffers] = await Promise.all([
db.donorOffer.findMany(query),
db.donorOffer.count({ where }),
]);
const [donorOffers, total] = await Promise.all([

And ensure line 482 returns:

return { donorOffers: filteredOffers, total };

But understand this will show incorrect pagination when "All Partners Responded" filter is active.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

should be fixed now in the fix two threads below this

Comment on lines +408 to +413
if (filters?.responseDeadline && filters.responseDeadline.type === "date") {
const dateCondition: Record<string, string> = {};
if (filters.responseDeadline.gte) dateCondition.gte = filters.responseDeadline.gte;
if (filters.responseDeadline.lte) dateCondition.lte = filters.responseDeadline.lte;
(where as Record<string, unknown>).partnerResponseDeadline = dateCondition;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Filter is mapped to wrong field - responseDeadline filter maps to partnerResponseDeadline, but it should map to donorResponseDeadline based on the frontend column id at AdminDonorOffersScreen.tsx:87 which is "donorResponseDeadline".

Suggested change
if (filters?.responseDeadline && filters.responseDeadline.type === "date") {
const dateCondition: Record<string, string> = {};
if (filters.responseDeadline.gte) dateCondition.gte = filters.responseDeadline.gte;
if (filters.responseDeadline.lte) dateCondition.lte = filters.responseDeadline.lte;
(where as Record<string, unknown>).partnerResponseDeadline = dateCondition;
}
if (filters?.donorResponseDeadline && filters.donorResponseDeadline.type === "date") {
const dateCondition: Record<string, string> = {};
if (filters.donorResponseDeadline.gte) dateCondition.gte = filters.donorResponseDeadline.gte;
if (filters.donorResponseDeadline.lte) dateCondition.lte = filters.donorResponseDeadline.lte;
(where as Record<string, unknown>).donorResponseDeadline = dateCondition;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Pretty confident the initial implementation is correct. There are two deadline columns within AdminDonorOffersScreen. There is responseDeadline on line 116 and donorResponseDeadline on line 131.

Comment on lines +471 to +480
let filteredOffers = mappedOffers;
if (filters?.allPartnersResponded && filters.allPartnersResponded.type === "enum") {
const wantAllPartnersResponded = filters.allPartnersResponded.values.includes("Yes");
filteredOffers = mappedOffers.filter((offer) => {
const allPartnersResponded = offer.invitedPartners.length > 0 &&
offer.invitedPartners.every((p) => p.responded);

return wantAllPartnersResponded ? allPartnersResponded : !allPartnersResponded
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Post-query filtering after pagination breaks expected behavior - filtering happens AFTER the database query (which already applied pagination), so users see incomplete results. For example, with 100 total offers where 20 match the filter, if pageSize=20, only the first 20 offers are queried, then filtered down to maybe 2-3 results, leaving 17+ empty slots on the page.

This needs to be either: 1) moved before the database query as a WHERE clause, or 2) the total count needs to reflect post-filter results and pagination recalculated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@greptile review

Copy link
Contributor

Choose a reason for hiding this comment

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

The allPartnersResponded filter is already correctly implemented at the database level (lines 426-451). The post-query filtering in lines 471-480 is redundant and should be removed:

Suggested change
let filteredOffers = mappedOffers;
if (filters?.allPartnersResponded && filters.allPartnersResponded.type === "enum") {
const wantAllPartnersResponded = filters.allPartnersResponded.values.includes("Yes");
filteredOffers = mappedOffers.filter((offer) => {
const allPartnersResponded = offer.invitedPartners.length > 0 &&
offer.invitedPartners.every((p) => p.responded);
return wantAllPartnersResponded ? allPartnersResponded : !allPartnersResponded
});
}
return {donorOffers: mappedOffers, total}

The database query already filters offers based on whether all partners responded, so applying the same filter again after pagination breaks the expected behavior. Remove lines 471-480 and just return the mapped offers directly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@greptile review

Copy link
Contributor

Choose a reason for hiding this comment

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

The redundant post-query filtering has been removed. The code now correctly returns results directly from the database query without refiltering:

return {donorOffers: mappedOffers, total}

The allPartnersResponded filter is properly implemented at the database level using the raw SQL query (lines 426-451), and there's no duplicate filtering after pagination. The issue is resolved.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yay

Comment on lines +52 to +61
if (filters?.value && filters?.value.type === "enum") {
const displayNames = filters.value.values;
const enumValueArr = Object.entries(shippingStatusToText);
const enumValues = displayNames
.map((name) => enumValueArr.find(([,text]) => text === name)?.[0])
.filter(Boolean);
(where as Record<string, unknown>).value = {
in: enumValues
};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Filters applied with where clause only used when isCompleted is undefined (line 143-150), but ignored when isCompleted is defined (line 70-141). This means status filters won't work on the shipments/signoffs tables that use the isCompleted parameter.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@greptile review

Copy link
Contributor

Choose a reason for hiding this comment

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

The issue I raised is valid - the status filter mapping you added (lines 52-61) only applies when isCompleted is undefined. When isCompleted is true or false (which is the case for shipments and sign-offs tables), the code takes a completely different path using raw SQL queries (lines 70-141), bypassing the where clause entirely.

This means status filtering won't work on the shipments/sign-offs tables that pass the isCompleted parameter. You need to either:

  1. Apply the display name → enum value mapping before calling getShipments(), or
  2. Incorporate filter logic into the raw SQL query path (more complex)

The simpler fix is option 1 - do the mapping in ShipmentsTable.tsx and SignOffsTable.tsx before passing filters to the service.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@greptile review

Copy link
Contributor

Choose a reason for hiding this comment

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

You've correctly fixed the issue! The status filter mapping now works on the raw SQL query path (lines 70-90 in shippingStatusService.ts:52-61), so filtering will work for both shipments and sign-offs tables. The implementation properly converts display names to enum values before injecting them into the SQL query. Good work!

@ezraexe ezraexe changed the title 280 advanced table filters Ezra/280 advanced table filters Feb 23, 2026
@ND68
Copy link
Collaborator

ND68 commented Feb 28, 2026

Overall most of the filters look great, good work.

Some issues with the Donor offers though:

  • The donor offers unfinalized table looks a bit too cluttered with the new columns. Please retain the filter functionality, but remove these columns from the table (or find a way to hide them): Partner Involved, Donor Response Deadline, All Partners Responded. Remove Partner Involved column from finalized and archived too.
  • The bounding boxes seem to be messed up. When you click the manage button, instead of the options laying over the table, the appear in the table, making the table scroll. Please fix this.
image

Everything else looks great.

@ezraexe
Copy link
Contributor Author

ezraexe commented Mar 1, 2026

Overall most of the filters look great, good work.

Some issues with the Donor offers though:

  • The donor offers unfinalized table looks a bit too cluttered with the new columns. Please retain the filter functionality, but remove these columns from the table (or find a way to hide them): Partner Involved, Donor Response Deadline, All Partners Responded. Remove Partner Involved column from finalized and archived too.
  • The bounding boxes seem to be messed up. When you click the manage button, instead of the options laying over the table, the appear in the table, making the table scroll. Please fix this.
image Everything else looks great.

Hid corresponding columns while keeping filter functionality
Image below:
image

Fixed the scrolling when opening manage dropdown

Image below:
image

@ND68 ND68 merged commit ff62fc1 into main Mar 4, 2026
5 checks passed
@ND68 ND68 deleted the 280-advanced-table-filters branch March 4, 2026 20:42
@ND68
Copy link
Collaborator

ND68 commented Mar 4, 2026

Revision was simple but effective, great work

@ND68 ND68 restored the 280-advanced-table-filters branch March 5, 2026 20:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Advanced Table Filters

2 participants