-
Notifications
You must be signed in to change notification settings - Fork 4
Manage Fertilizer Applications via Badge Interaction in Fields and Rotation Tables #475
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
0815269
Add modify fertilizer applications dialog
BoraIneviNMI d67a7d6
Fix grouping and a few other things
BoraIneviNMI b6f7cd5
Add changeset
BoraIneviNMI 2f4b12e
Add empty table fallback and fix the routes for farm create wizard
BoraIneviNMI 5d265cd
Merge branch 'development' into FDM440
BoraIneviNMI c01b075
Await transactions
BoraIneviNMI 6c65c83
Make appIds search param use consistent
BoraIneviNMI 2d4e755
Add spinner when deleting fertilizer application
BoraIneviNMI 2ef5eee
Nitpicks
BoraIneviNMI c84cdef
Merge branch 'FDM440' of github.com:SvenVw/fdm into FDM440
BoraIneviNMI f135c32
Fix partial updates code and resolve some nitpicks
BoraIneviNMI d1a6df6
Merge branch 'development' into FDM440
BoraIneviNMI 0fa2bfe
Improve type safety when extracting form data
BoraIneviNMI fbc31db
Fix merge error
BoraIneviNMI 0863517
Address comments
BoraIneviNMI 20a4c61
Add grouping method selector
BoraIneviNMI 7cabdc3
Change some things
BoraIneviNMI 38bbdd8
Add field names column and various conditional messages
BoraIneviNMI 484e7db
Use react table for the fertilizer application table and add it to th…
BoraIneviNMI 38fda10
Nitpicks
BoraIneviNMI a7b8363
Oops
BoraIneviNMI 3927d21
Group by similar applications and remove option to choose grouping
BoraIneviNMI b014f42
Add conditional rendering of the fertilizer application modification …
BoraIneviNMI 57c3dfc
Add documentation
BoraIneviNMI 6a083a0
Use field fertilizer add page since it is more functionally similar
BoraIneviNMI 6119a3e
Nitpicks in column.tsx
BoraIneviNMI 8c1b124
Nitpicks on the field fertilizer add page
BoraIneviNMI f6f8b22
Harden isOfOrigin
BoraIneviNMI 16c7cdb
Remove memoization from table cells
BoraIneviNMI 31401ef
Import format
BoraIneviNMI 3d4cedc
Change some logic
BoraIneviNMI e8c4cdf
Harden isOfOrigin
BoraIneviNMI 9e67b85
Merge branch 'development' into FDM440
BoraIneviNMI ceae844
Nitpicks
BoraIneviNMI 40d61b3
Nitpicks
BoraIneviNMI 416619a
Add more documentation
BoraIneviNMI b638a17
fix: text improvements
SvenVw 2dbf929
refactor: move FertilizerApplicationListDialog into seperate componen…
SvenVw bb3c4aa
fix: Empty-state text is singular in a possible multi-field context
SvenVw File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@svenvw/fdm-app": minor | ||
| --- | ||
|
|
||
| Users can click the fertilizer badges on the rotation table to see a table of applications of this fertilizer onto the clicked cultivation or field. They can edit the applications directly from this table. |
262 changes: 262 additions & 0 deletions
262
fdm-app/app/components/blocks/fertilizer-applications/columns.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,262 @@ | ||
| import type { FertilizerApplication } from "@nmi-agro/fdm-core" | ||
| import type { CellContext, ColumnDef } from "@tanstack/react-table" | ||
| import { endOfDay, format } from "date-fns" | ||
| import { nl } from "date-fns/locale" | ||
| import { ChevronDown } from "lucide-react" | ||
| import { useState } from "react" | ||
| import { NavLink, useFetcher, useParams } from "react-router" | ||
| import { cn } from "@/app/lib/utils" | ||
| import { | ||
| AlertDialog, | ||
| AlertDialogAction, | ||
| AlertDialogCancel, | ||
| AlertDialogContent, | ||
| AlertDialogDescription, | ||
| AlertDialogFooter, | ||
| AlertDialogHeader, | ||
| AlertDialogTitle, | ||
| } from "~/components/ui/alert-dialog" | ||
| import { Button } from "~/components/ui/button" | ||
| import { | ||
| DropdownMenu, | ||
| DropdownMenuContent, | ||
| DropdownMenuItem, | ||
| DropdownMenuTrigger, | ||
| } from "~/components/ui/dropdown-menu" | ||
| import { ScrollArea } from "~/components/ui/scroll-area" | ||
| import { Spinner } from "~/components/ui/spinner" | ||
|
|
||
| export type ApplicationExtended = FertilizerApplication & { | ||
| b_id: string | ||
| b_name: string | ||
| p_app_method_name: string | null | undefined | ||
| canModify: boolean | ||
| } | ||
|
|
||
| export interface FertAppRecordItem { | ||
| id: string | ||
| applications: ApplicationExtended[] | ||
| } | ||
|
|
||
| /** | ||
| * Stringifies a single date if the given range is all the same dates, | ||
| * otherwise stringifies the earliest and the latest date with a dash in between. | ||
| * | ||
| * @param dates array of dates. Nulls and undefined items are not allowed. | ||
| * @returns the formatted string. | ||
| */ | ||
| function formatDateRange(dates: Date[]) { | ||
| if (dates.length === 0) return "" | ||
| const firstDate = dates[0] | ||
| const lastDate = dates[dates.length - 1] | ||
| return createDateKey(firstDate) === createDateKey(lastDate) | ||
| ? `${format(firstDate, "PP", { locale: nl })}` | ||
| : `${format(firstDate, "PP", { locale: nl })} - ${format(lastDate, "PP", { locale: nl })}` | ||
| } | ||
|
|
||
| /** | ||
| * Stringifies a single number with an unit if the given range is all the same numbers or very close down to roughly 2 decimal places, | ||
| * otherwise stringifies the least and the greatest numbers with a dash in between and the unit at the end. | ||
| * | ||
| * @param numbers array of numbers. Nulls and undefined items are not allowed. | ||
| * @returns the formatted string. | ||
| */ | ||
| function formatNumberRange(numbers: number[], unit = "") { | ||
| if (numbers.length === 0) return "" | ||
| const firstNumber = numbers[0] | ||
| const lastNumber = numbers[numbers.length - 1] | ||
| return firstNumber === lastNumber || | ||
| Math.abs(lastNumber - firstNumber) < Math.abs(firstNumber) / 100 | ||
| ? `${firstNumber} ${unit}` | ||
| : `${firstNumber} - ${lastNumber} ${unit}` | ||
| } | ||
|
|
||
| /** | ||
| * Returns a date string that doesn't contain the time. | ||
| * | ||
| * The app interface doesn't really show the time in the day for dates, so we ignore the time this way */ | ||
| export function createDateKey(date: Date) { | ||
| return format(endOfDay(date), "yyyy-MM-dd") | ||
| } | ||
|
|
||
| export const columns: ColumnDef<FertAppRecordItem>[] = [ | ||
| { | ||
| id: "p_app_date", | ||
| header: "Datum", | ||
| cell: ({ row }) => | ||
| formatDateRange( | ||
| row.original.applications.map((app) => app.p_app_date), | ||
| ), | ||
| }, | ||
| { | ||
| id: "applications.length", | ||
| accessorKey: "applications.length", | ||
| header: "Aantal bemestingen", | ||
| }, | ||
| { | ||
| id: "b_name", | ||
| header: "Perceel", | ||
| cell: ({ row }) => { | ||
| const fieldNames = Object.entries( | ||
| Object.fromEntries( | ||
| row.original.applications.map((app) => [ | ||
| app.b_id, | ||
| app.b_name, | ||
| ]), | ||
| ), | ||
| ).sort((a, b) => (a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0)) | ||
| if (fieldNames.length <= 1) { | ||
| return fieldNames[0]?.[1] ?? "" | ||
| } | ||
| return ( | ||
| <DropdownMenu> | ||
| <DropdownMenuTrigger asChild> | ||
| <Button variant="ghost" className="-ms-4"> | ||
| <span className="text-muted-foreground"> | ||
| {fieldNames.length} percelen | ||
| </span> | ||
| <ChevronDown className="ml-1 size-3" /> | ||
| </Button> | ||
| </DropdownMenuTrigger> | ||
| <DropdownMenuContent> | ||
| <ScrollArea | ||
| className={ | ||
| fieldNames.length >= 8 | ||
| ? "h-72 overflow-y-auto w-48" | ||
| : "w-48" | ||
| } | ||
| > | ||
| <div className="grid grid-cols-1 gap-2"> | ||
| {fieldNames.map(([b_id, b_name]) => ( | ||
| <DropdownMenuItem key={b_id}> | ||
| {b_name} | ||
| </DropdownMenuItem> | ||
| ))} | ||
| </div> | ||
| </ScrollArea> | ||
| </DropdownMenuContent> | ||
| </DropdownMenu> | ||
| ) | ||
| }, | ||
| }, | ||
| { | ||
| id: "p_app_method", | ||
| header: "Toedieningsmethode", | ||
| cell: ({ row }) => | ||
| [ | ||
| ...new Set( | ||
| row.original.applications | ||
| .map( | ||
| (application) => | ||
| application.p_app_method_name ?? | ||
| application.p_app_method, | ||
| ) | ||
| .filter((methodName) => methodName !== null), | ||
| ), | ||
| ] | ||
| .sort() | ||
| .join(", "), | ||
| }, | ||
| { | ||
| id: "p_app_amount", | ||
| header: "Hoeveelheid", | ||
| cell: ({ row }) => | ||
| formatNumberRange( | ||
| row.original.applications | ||
| .map((application) => application.p_app_amount) | ||
| .filter((amount) => amount !== null) | ||
| .sort((a, b) => a - b), | ||
| "kg / ha", | ||
| ), | ||
| }, | ||
| { | ||
| id: "modify", | ||
| header: "", | ||
| cell: (ctx) => | ||
| ctx.row.original.applications.some((app) => app.canModify) && ( | ||
| <ModifyCell {...ctx} /> | ||
| ), | ||
| }, | ||
| ] | ||
|
|
||
| /** | ||
| * Renders buttons that let the user edit or remove the applications found in the row record. | ||
| * | ||
| * The edit button will navigate to the add/update fertilizer page with the necessary search params. | ||
| * | ||
| * The delete button will show a confirmation dialog before deleting the fertilizer application(s). | ||
| * | ||
| * @param param0 all of the React Table cell context | ||
| * @returns a React node that can be set as the cell contents | ||
| */ | ||
| function ModifyCell({ row, table }: CellContext<FertAppRecordItem, unknown>) { | ||
| const params = useParams() | ||
| const fetcher = useFetcher() | ||
| const returnUrl = (table.options.meta as { returnUrl: string }).returnUrl | ||
| const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) | ||
|
|
||
| const modifiableApps = row.original.applications.filter( | ||
| (app) => app.canModify, | ||
| ) | ||
| const modifiableAppIds = modifiableApps | ||
| .map((app) => `${app.b_id}:${app.p_app_id}`) | ||
| .join(",") | ||
|
|
||
| return ( | ||
| <div className="flex items-center justify-end gap-2"> | ||
| <Spinner | ||
| className={cn( | ||
| "h-4 w-4", | ||
| fetcher.state !== "submitting" && "invisible", | ||
| )} | ||
| /> | ||
| <Button asChild> | ||
| <NavLink | ||
| to={`/farm/${params.b_id_farm}/${params.calendar}/field/fertilizer?appIds=${encodeURIComponent(modifiableAppIds)}&returnUrl=${encodeURIComponent(returnUrl)}`} | ||
| > | ||
| Wijzigen | ||
| </NavLink> | ||
| </Button> | ||
| <Button | ||
| variant="destructive" | ||
| onClick={() => setShowDeleteConfirm(true)} | ||
| disabled={fetcher.state === "submitting"} | ||
| > | ||
| Verwijderen | ||
| </Button> | ||
| <AlertDialog | ||
| open={showDeleteConfirm} | ||
| onOpenChange={setShowDeleteConfirm} | ||
| > | ||
| <AlertDialogContent> | ||
| <AlertDialogHeader> | ||
| <AlertDialogTitle> | ||
| {modifiableApps.length === 1 | ||
| ? "Bemesting verwijderen?" | ||
| : `${modifiableApps.length} bemestingen verwijderen?`} | ||
| </AlertDialogTitle> | ||
| <AlertDialogDescription> | ||
| {modifiableApps.length === 1 | ||
| ? "Weet je zeker dat je deze bemesting wilt verwijderen? Dit kan niet ongedaan worden gemaakt." | ||
| : `Weet je zeker dat je deze ${modifiableApps.length} bemestingen wilt verwijderen? Dit kan niet ongedaan worden gemaakt.`} | ||
| </AlertDialogDescription> | ||
| </AlertDialogHeader> | ||
| <AlertDialogFooter> | ||
| <AlertDialogCancel>Annuleren</AlertDialogCancel> | ||
| <AlertDialogAction | ||
| className="bg-destructive text-destructive-foreground hover:bg-destructive/90" | ||
| onClick={() => { | ||
| const formData = new FormData() | ||
| formData.set("appIds", modifiableAppIds) | ||
| formData.set("intent", "remove_application") | ||
| fetcher.submit(formData, { method: "POST" }) | ||
| }} | ||
| > | ||
| Verwijderen | ||
| </AlertDialogAction> | ||
| </AlertDialogFooter> | ||
| </AlertDialogContent> | ||
| </AlertDialog> | ||
| </div> | ||
| ) | ||
| } | ||
106 changes: 106 additions & 0 deletions
106
fdm-app/app/components/blocks/fertilizer-applications/dialog.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| import type { ApplicationExtended } from "~/components/blocks/fertilizer-applications/columns" | ||
| import { DataTable } from "~/components/blocks/fertilizer-applications/table" | ||
| import { Button } from "~/components/ui/button" | ||
| import { | ||
| Dialog, | ||
| DialogClose, | ||
| DialogContent, | ||
| DialogDescription, | ||
| DialogFooter, | ||
| DialogHeader, | ||
| DialogTitle, | ||
| } from "~/components/ui/dialog" | ||
| import { | ||
| Empty, | ||
| EmptyDescription, | ||
| EmptyHeader, | ||
| EmptyTitle, | ||
| } from "~/components/ui/empty" | ||
| import { ScrollArea } from "~/components/ui/scroll-area" | ||
|
|
||
| interface FertilizerInfo { | ||
| p_id: string | ||
| p_name_nl: string | null | ||
| } | ||
|
|
||
| interface FertilizerApplicationListDialogProps { | ||
| isForRotation: boolean | ||
| numFields: number | ||
| fertilizer: FertilizerInfo | ||
| fertilizerApplications: ApplicationExtended[][] | ||
| returnUrl: string | ||
| onClose: () => void | ||
| } | ||
|
|
||
| export function FertilizerApplicationListDialog({ | ||
| isForRotation, | ||
| numFields, | ||
| fertilizer, | ||
| fertilizerApplications, | ||
| returnUrl, | ||
| onClose, | ||
| }: FertilizerApplicationListDialogProps) { | ||
| const numFertilizerApplications = fertilizerApplications | ||
| .map((apps) => apps.length) | ||
| .reduce((a, b) => a + b, 0) | ||
|
|
||
| const fieldNameToShow = | ||
| numFields === 1 | ||
| ? fertilizerApplications.find((apps) => apps.length > 0)?.[0].b_name | ||
| : undefined | ||
|
|
||
| const titleSuffix = fieldNameToShow | ||
| ? ` op ${fieldNameToShow}` | ||
| : numFields > 1 | ||
| ? ` op ${numFields} percelen` | ||
| : undefined | ||
|
|
||
| return ( | ||
| <Dialog open={true} onOpenChange={onClose}> | ||
| <DialogContent className="max-w-4xl transition-transform duration-1000"> | ||
| <DialogHeader> | ||
| <DialogTitle> | ||
| {fertilizer.p_name_nl} | ||
| {titleSuffix} | ||
| </DialogTitle> | ||
| <DialogDescription> | ||
| Bekijk en beheer de bemestingen met deze meststof. | ||
| </DialogDescription> | ||
| </DialogHeader> | ||
| {numFertilizerApplications > 0 ? ( | ||
| <ScrollArea className="max-h-[60vh]"> | ||
| <DataTable | ||
| numFields={numFields} | ||
| fertilizerApplications={fertilizerApplications} | ||
| returnUrl={returnUrl} | ||
| /> | ||
| </ScrollArea> | ||
| ) : ( | ||
| <Empty> | ||
| <EmptyHeader> | ||
| <EmptyTitle>Geen bemestingen gevonden</EmptyTitle> | ||
| <EmptyDescription> | ||
| {isForRotation | ||
| ? "Deze meststof wordt niet langer op dit perceel/deze percelen en gewassen toegepast." | ||
| : numFields > 1 | ||
| ? "Deze meststof wordt niet langer op deze percelen toegepast." | ||
| : "Deze meststof wordt niet langer op dit perceel toegepast."} | ||
| </EmptyDescription> | ||
| <EmptyDescription> | ||
| Sluit dit venster om een nieuwe bemesting toe te | ||
| voegen. | ||
| </EmptyDescription> | ||
| </EmptyHeader> | ||
| </Empty> | ||
| )} | ||
| <DialogFooter> | ||
| <DialogClose asChild> | ||
| <Button variant="outline" type="button"> | ||
| Sluiten | ||
| </Button> | ||
| </DialogClose> | ||
| </DialogFooter> | ||
| </DialogContent> | ||
| </Dialog> | ||
| ) | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.