Skip to content

Commit

Permalink
aircraft page with categories
Browse files Browse the repository at this point in the history
  • Loading branch information
vsimakhin committed Feb 13, 2025
1 parent 0bf0ca1 commit de485ca
Show file tree
Hide file tree
Showing 22 changed files with 644 additions and 79 deletions.
65 changes: 65 additions & 0 deletions app/handlers_aircraft.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package main

import (
"encoding/json"
"fmt"
"net/http"
"sort"
"strings"

"github.com/go-chi/chi/v5"
"github.com/vsimakhin/web-logbook/internal/models"
Expand Down Expand Up @@ -45,6 +48,12 @@ func (app *application) HandlerApiAircraftList(w http.ResponseWriter, r *http.Re
return
}

err = app.db.GenerateAircraftCategoriesTable()
if err != nil {
app.handleError(w, err)
return
}

aircrafts, err := app.db.GetAircrafts()
if err != nil {
app.handleError(w, err)
Expand All @@ -53,3 +62,59 @@ func (app *application) HandlerApiAircraftList(w http.ResponseWriter, r *http.Re

app.writeJSON(w, http.StatusOK, aircrafts)
}

// HandlerApiAircraftModelsCategories is a handler for getting the list of aircraft categories
func (app *application) HandlerApiAircraftModelsCategoriesList(w http.ResponseWriter, r *http.Request) {
categories, err := app.db.GetAircraftModelsCategories()
if err != nil {
app.handleError(w, err)
return
}

app.writeJSON(w, http.StatusOK, categories)
}

// HandlerApiAircraftModelsCategoriesUpdate is a handler for updating aircraft categories
func (app *application) HandlerApiAircraftModelsCategoriesUpdate(w http.ResponseWriter, r *http.Request) {
var category models.Category
err := json.NewDecoder(r.Body).Decode(&category)
if err != nil {
app.handleError(w, err)
return
}

err = app.db.UpdateAircraftModelsCategories(category)
if err != nil {
app.handleError(w, err)
return
}

app.writeOkResponse(w, "Aircraft categories have been updated")
}

// HandlerApiAircraftCategoriesList is a handler for getting the list of aircraft categories
func (app *application) HandlerApiAircraftCategoriesList(w http.ResponseWriter, r *http.Request) {
categories, err := app.db.GetAircraftModelsCategories()
if err != nil {
app.handleError(w, err)
return
}

var categorySet = make(map[string]bool)
for _, aircraft := range categories {
for _, cat := range strings.Split(aircraft.Category, ",") {
cat = strings.TrimSpace(cat)
categorySet[cat] = true
}
}

var cats []string
for c := range categorySet {
if c != "" {
cats = append(cats, c)
}
}
sort.Strings(cats)

app.writeJSON(w, http.StatusOK, cats)
}
3 changes: 3 additions & 0 deletions app/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ func (app *application) routes() *chi.Mux {
r.Route("/aircraft", func(r chi.Router) {
r.Get("/list", app.HandlerApiAircraftList)
r.Get("/models", app.HandlerApiAircraftModels)
r.Get("/models-categories", app.HandlerApiAircraftModelsCategoriesList)
r.Put("/models-categories", app.HandlerApiAircraftModelsCategoriesUpdate)
r.Get("/categories", app.HandlerApiAircraftCategoriesList)
r.Get("/logbook", app.HandlerAircrafts)
r.Get("/logbook/{filter}", app.HandlerAircrafts)
})
Expand Down
9 changes: 6 additions & 3 deletions app/ui/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Outlet } from 'react-router-dom';
import { QueryClientProvider } from '@tanstack/react-query';
import { AppProvider } from '@toolpad/core/react-router-dom';
import { NotificationsProvider } from '@toolpad/core/useNotifications';
import { DialogsProvider } from '@toolpad/core/useDialogs';
// MUI UI elements
import { createTheme } from '@mui/material/styles';
import { LocalizationProvider } from '@mui/x-date-pickers';
Expand All @@ -26,9 +27,11 @@ function App() {
<AppProvider theme={theme} navigation={NAV_ITEMS} branding={BRANDING} >
<LocalizationProvider dateAdapter={AdapterDayjs}>
<QueryClientProvider client={queryClient}>
<NotificationsProvider>
<Outlet />
</NotificationsProvider>
<DialogsProvider>
<NotificationsProvider>
<Outlet />
</NotificationsProvider>
</DialogsProvider>
</QueryClientProvider>
</LocalizationProvider>
</AppProvider>
Expand Down
37 changes: 37 additions & 0 deletions app/ui/src/components/Aircrafts/AircraftCategories.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
// Custom
import Select from "../UIElements/Select";
import { fetchAircraftCategories } from "../../util/http/aircraft";

export const AircraftCategories = ({ gsize, value, handleChange }) => {
const navigate = useNavigate();
const [options, setOptions] = useState([])

const { data: categories } = useQuery({
queryFn: ({ signal }) => fetchAircraftCategories({ signal, navigate }),
queryKey: ['aircraft-categories'],
})

useEffect(() => {
if (categories) {
setOptions(categories);
}
}, [categories])

return (
<Select gsize={gsize}
id="category"
label="Category"
handleChange={handleChange}
value={value}
tooltip={"Aircraft Category"}
options={options}
multiple
freeSolo={true}
/>
);
}

export default AircraftCategories;
32 changes: 6 additions & 26 deletions app/ui/src/components/Aircrafts/Aircrafts.jsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,31 @@
import { useNavigate } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import { useEffect, useState } from "react";
// MUI
import Grid from "@mui/material/Grid2";
import LinearProgress from '@mui/material/LinearProgress';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
// Custom
import { useErrorNotification } from "../../hooks/useAppNotifications";
import CardHeader from "../UIElements/CardHeader";
import { fetchAircrafts } from "../../util/http/aircraft";
import AircraftsTable from "./AircraftsTable";
import CategoriesTable from "./CategoriesTable";

export const Aircrafts = () => {
const navigate = useNavigate();

const { data, isLoading, isError, error } = useQuery({
queryKey: ['aircrafts'],
queryFn: ({ signal }) => fetchAircrafts({ signal, navigate }),
});
useErrorNotification({ isError, error, fallbackMessage: 'Failed to load aircrafts' });

return (
<>
{isLoading && <LinearProgress />}
<Grid container spacing={1} >
<Grid size={{ xs: 12, sm: 12, md: 6, lg: 6, xl: 6 }}>
<Card variant="outlined" sx={{ mb: 1 }}>
<CardContent>
<CardHeader title="Aircrafts"
action={
<>
</>
}
/>
<CardHeader title="Aircrafts" />
<AircraftsTable />
</CardContent>
</Card >
</Grid>

<Grid size={{ xs: 12, sm: 12, md: 6, lg: 6, xl: 6 }}>
<Card variant="outlined" sx={{ mb: 1 }}>
<CardContent>
<CardHeader title="Categories"
action={
<>
</>
}
/>
<CardHeader title="Types & Categories" />
<CategoriesTable />
</CardContent>
</Card >
</Grid>
Expand Down
102 changes: 102 additions & 0 deletions app/ui/src/components/Aircrafts/AircraftsTable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { MaterialReactTable, useMaterialReactTable, MRT_TableHeadCellFilterContainer, MRT_ExpandAllButton } from 'material-react-table';
import { useMemo, useState } from 'react';
import { useLocalStorageState } from '@toolpad/core/useLocalStorageState';
import { useNavigate } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
// MUI UI elements
import Box from '@mui/material/Box';
import Drawer from '@mui/material/Drawer';
import LinearProgress from '@mui/material/LinearProgress';
// Custom components and libraries
import CSVAircraftExportButton from './CSVAircraftExportButton';
import { tableJSONCodec } from '../../constants/constants';
import { fetchAircrafts } from "../../util/http/aircraft";
import { useErrorNotification } from "../../hooks/useAppNotifications";
import { dateFilterFn } from '../../util/helpers';

const paginationKey = 'aircrafts-table-page-size';
const columnVisibilityKey = 'aircrafts-table-column-visibility';

const tableOptions = {
initialState: { density: 'compact' },
positionToolbarAlertBanner: 'bottom',
groupedColumnMode: 'remove',
enableColumnResizing: true,
enableGlobalFilterModes: true,
enableColumnFilters: true,
enableColumnDragging: false,
enableColumnPinning: false,
enableGrouping: true,
enableDensityToggle: false,
columnResizeMode: 'onEnd',
muiTablePaperProps: { variant: 'outlined', elevation: 0 },
columnFilterDisplayMode: 'custom',
enableFacetedValues: true,
enableSorting: true,
enableColumnActions: true,
}

export const AircraftsTable = ({ ...props }) => {
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);
const [columnFilters, setColumnFilters] = useState([]);
const [columnVisibility, setColumnVisibility] = useLocalStorageState(columnVisibilityKey, {}, { codec: tableJSONCodec });
const [pagination, setPagination] = useLocalStorageState(paginationKey, { pageIndex: 0, pageSize: 15 }, { codec: tableJSONCodec });
const filterFns = useMemo(() => ({ dateFilterFn: dateFilterFn }), []);

const navigate = useNavigate();

const { data, isLoading, isError, error } = useQuery({
queryKey: ['aircrafts'],
queryFn: ({ signal }) => fetchAircrafts({ signal, navigate }),
});
useErrorNotification({ isError, error, fallbackMessage: 'Failed to load aircrafts' });

const columns = useMemo(() => [
{ accessorKey: "reg", header: "Registration", size: 120 },
{ accessorKey: "model", header: "Type", size: 110 },
{ accessorKey: "category", header: "Category", grow: true },
], []);

const table = useMaterialReactTable({
isLoading: isLoading,
columns: columns,
data: data ?? [],
onShowColumnFiltersChange: () => (setIsFilterDrawerOpen(true)),
filterFns: filterFns,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
renderTopToolbarCustomActions: ({ table }) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap' }}>
<CSVAircraftExportButton table={table} />
</Box>
),
onPaginationChange: setPagination,
state: { pagination, columnFilters: columnFilters, columnVisibility },
defaultColumn: {
muiFilterTextFieldProps: ({ column }) => ({ label: `Filter by ${column.columnDef.header}` }),
},
...tableOptions
});

return (
<>
{isLoading && <LinearProgress />}
<MaterialReactTable table={table} {...props} />
<Drawer anchor="right" open={isFilterDrawerOpen} onClose={() => setIsFilterDrawerOpen(false)} sx={{
'& .MuiDrawer-paper': {
marginTop: '64px',
height: 'calc(100% - 64px)',
},
}}>
<Box sx={{ width: 350, padding: 2 }}>
{table.getLeafHeaders().map((header) => {
if (header.id.startsWith('mrt-') || header.id.startsWith('Expire') || header.id.startsWith('center_1_')) return null;
return < MRT_TableHeadCellFilterContainer key={header.id} header={header} table={table} in />
})}
</Box>
</Drawer>
</>
);
}

export default AircraftsTable;
39 changes: 39 additions & 0 deletions app/ui/src/components/Aircrafts/CSVAircraftExportButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useCallback } from 'react';
// MUI UI elements
import Tooltip from '@mui/material/Tooltip';
import IconButton from '@mui/material/IconButton';
// MUI Icons
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';

import { mkConfig, generateCsv, download } from 'export-to-csv';

const csvConfig = mkConfig({
fieldSeparator: ',',
decimalSeparator: '.',
useKeysAsHeaders: true,
filename: 'aircrafts',
});

const handleExportRows = (rows) => {
const rowData = rows.map((row) => ({
"Registration": row.original.reg,
"Model": row.original.model,
"Category": row.original.category,
}));
const csv = generateCsv(csvConfig)(rowData);
download(csvConfig)(csv);
};

export const CSVAircraftExportButton = ({ table }) => {
const handleCSVExport = useCallback((table) => {
handleExportRows(table.getPrePaginationRowModel().rows);
}, []);

return (
<Tooltip title="Quick CSV Export">
<IconButton onClick={() => handleCSVExport(table)} size="small"><FileDownloadOutlinedIcon /></IconButton>
</Tooltip>
)
}

export default CSVAircraftExportButton;
38 changes: 38 additions & 0 deletions app/ui/src/components/Aircrafts/CSVCategoriesExportButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useCallback } from 'react';
// MUI UI elements
import Tooltip from '@mui/material/Tooltip';
import IconButton from '@mui/material/IconButton';
// MUI Icons
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';

import { mkConfig, generateCsv, download } from 'export-to-csv';

const csvConfig = mkConfig({
fieldSeparator: ',',
decimalSeparator: '.',
useKeysAsHeaders: true,
filename: 'categories',
});

const handleExportRows = (rows) => {
const rowData = rows.map((row) => ({
"Type": row.original.model,
"Category": row.original.category,
}));
const csv = generateCsv(csvConfig)(rowData);
download(csvConfig)(csv);
};

export const CSVCategoriesExportButton = ({ table }) => {
const handleCSVExport = useCallback((table) => {
handleExportRows(table.getPrePaginationRowModel().rows);
}, []);

return (
<Tooltip title="Quick CSV Export">
<IconButton onClick={() => handleCSVExport(table)} size="small"><FileDownloadOutlinedIcon /></IconButton>
</Tooltip>
)
}

export default CSVCategoriesExportButton;
Loading

0 comments on commit de485ca

Please sign in to comment.