diff --git a/app/Http/Controllers/CarrierController.php b/app/Http/Controllers/CarrierController.php index 8427911..3d1d086 100644 --- a/app/Http/Controllers/CarrierController.php +++ b/app/Http/Controllers/CarrierController.php @@ -7,6 +7,8 @@ use App\Http\Requests\UpdateCarrierRequest; use App\Http\Resources\CarrierResource; use App\Models\Carrier; +use Illuminate\Http\Request; +use Inertia\Inertia; class CarrierController extends ResourceSearchController { @@ -15,9 +17,20 @@ class CarrierController extends ResourceSearchController /** * Display a listing of the resource. */ - public function index() + public function index(Request $request) { - // + $request->validate([ + 'carrier_id' => 'nullable|exists:carriers,id', + ]); + + if ($request->input('carrier_id')) { + $carrier = Carrier::find($request->input('carrier_id')); + return Inertia::render('Carriers/Index', [ + 'carrier' => CarrierResource::make($carrier), + ]); + } + + return Inertia::render('Carriers/Index'); } /** diff --git a/app/Http/Controllers/FacilityController.php b/app/Http/Controllers/FacilityController.php index a622bb7..b09230b 100644 --- a/app/Http/Controllers/FacilityController.php +++ b/app/Http/Controllers/FacilityController.php @@ -9,8 +9,26 @@ use App\Models\Facility; use App\Models\Location; use Illuminate\Http\Request; +use Inertia\Inertia; + class FacilityController extends ResourceSearchController { protected $model = Facility::class; protected $modelResource = FacilityResource::class; + + public function index(Request $request) + { + $request->validate([ + 'facility_id' => 'nullable|exists:facilities,id', + ]); + + if ($request->input('facility_id')) { + $facility = Facility::find($request->input('facility_id')); + return Inertia::render('Facilities/Index', [ + 'facility' => FacilityResource::make($facility), + ]); + } + + return Inertia::render('Facilities/Index'); + } } diff --git a/app/Http/Controllers/ShipperController.php b/app/Http/Controllers/ShipperController.php index 5c05ff7..8519e18 100644 --- a/app/Http/Controllers/ShipperController.php +++ b/app/Http/Controllers/ShipperController.php @@ -7,6 +7,8 @@ use App\Http\Requests\UpdateShipperRequest; use App\Http\Resources\ShipperResource; use App\Models\Shipper; +use Illuminate\Http\Request; +use Inertia\Inertia; class ShipperController extends ResourceSearchController { @@ -15,9 +17,20 @@ class ShipperController extends ResourceSearchController /** * Display a listing of the resource. */ - public function index() + public function index(Request $request) { - // + $request->validate([ + 'shipper_id' => 'nullable|exists:shippers,id', + ]); + + if ($request->input('shipper_id')) { + $shipper = Shipper::find($request->input('shipper_id')); + return Inertia::render('Shippers/Index', [ + 'shipper' => ShipperResource::make($shipper), + ]); + } + + return Inertia::render('Shippers/Index'); } /** diff --git a/resources/js/Components/AppSidebar.tsx b/resources/js/Components/AppSidebar.tsx index 6953e2d..33852c8 100644 --- a/resources/js/Components/AppSidebar.tsx +++ b/resources/js/Components/AppSidebar.tsx @@ -1,4 +1,12 @@ -import { Building, GalleryVerticalEnd, Home, Truck } from 'lucide-react'; +import { + Building, + GalleryVerticalEnd, + Home, + Package, + Truck, + Users, + Warehouse, +} from 'lucide-react'; import * as React from 'react'; import { NavUser } from '@/Components/NavUser'; @@ -58,10 +66,38 @@ export function AppSidebar({ ...props }: React.ComponentProps) { isActive={route().current('shipments.index')} > - + Shipments + + + + Carriers + + + + + + Shippers + + + + + + Facilities + + + {(permissions.ORGANIZATION_MANAGER || permissions.ORGANIZATION_MANAGE_USERS) && ( void; +}) { + const [data, setData] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); + const inputRef = useRef(null); + + const getCarriers = useCallback((searchTerm?: string) => { + const getData = (): Promise => { + return axios + .get(route('carriers.search'), { + params: { + query: searchTerm, + with: [], + }, + }) + .then((response) => response.data); + }; + + setIsLoading(true); + + getData() + .then((carriers) => { + setData(carriers); + setIsLoading(false); + }) + .catch((error) => { + console.error('Error fetching carriers:', error); + setIsLoading(false); + }); + }, []); + + useEffect(() => { + if (!isLoading) { + inputRef.current?.focus(); + } + }, [isLoading]); + + useEffect(() => { + getCarriers(); + }, [getCarriers]); + + return ( +
+
+ setSearchTerm(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + getCarriers(searchTerm); + } + }} + /> + +
+ {isLoading ? ( + <> + + + ) : ( + <> + + + )} +
+ ); +} diff --git a/resources/js/Components/Carriers/CarrierList/Columns.tsx b/resources/js/Components/Carriers/CarrierList/Columns.tsx new file mode 100644 index 0000000..24689c1 --- /dev/null +++ b/resources/js/Components/Carriers/CarrierList/Columns.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { Carrier } from '@/types'; +import { ColumnDef } from '@tanstack/react-table'; + +export const columns: ColumnDef[] = [ + { + accessorKey: 'name', + header: 'Name', + cell: ({ row }) => { + return ( +
+ {row.original.name} +
+ ); + }, + }, +]; diff --git a/resources/js/Components/Carriers/CarrierList/DataTable.tsx b/resources/js/Components/Carriers/CarrierList/DataTable.tsx new file mode 100644 index 0000000..84bacaf --- /dev/null +++ b/resources/js/Components/Carriers/CarrierList/DataTable.tsx @@ -0,0 +1,92 @@ +'use client'; + +import { + ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, +} from '@tanstack/react-table'; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/Components/ui/table'; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + onSelect: (carrier: TData) => void; +} + +export function DataTable({ + columns, + data, + onSelect, +}: DataTableProps) { + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + }); + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef + .header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + { + onSelect(row.original); + }} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+ ); +} diff --git a/resources/js/Components/Facilities/FacilityList/Columns.tsx b/resources/js/Components/Facilities/FacilityList/Columns.tsx new file mode 100644 index 0000000..2259191 --- /dev/null +++ b/resources/js/Components/Facilities/FacilityList/Columns.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { Facility } from '@/types'; +import { ColumnDef } from '@tanstack/react-table'; + +export const columns: ColumnDef[] = [ + { + accessorKey: 'name', + header: 'Name', + cell: ({ row }) => { + return ( +
+ {row.original.name} +
+ ); + }, + }, +]; diff --git a/resources/js/Components/Facilities/FacilityList/DataTable.tsx b/resources/js/Components/Facilities/FacilityList/DataTable.tsx new file mode 100644 index 0000000..b27ce2a --- /dev/null +++ b/resources/js/Components/Facilities/FacilityList/DataTable.tsx @@ -0,0 +1,93 @@ +'use client'; + +import { + ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, +} from '@tanstack/react-table'; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/Components/ui/table'; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + onSelect: (shipper: TData) => void; +} + +export function DataTable({ + columns, + data, + onSelect, +}: DataTableProps) { + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + }); + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef + .header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + { + onSelect(row.original); + }} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+ ); +} diff --git a/resources/js/Components/Facilities/FacilityList/FacilityList.tsx b/resources/js/Components/Facilities/FacilityList/FacilityList.tsx new file mode 100644 index 0000000..23fd68a --- /dev/null +++ b/resources/js/Components/Facilities/FacilityList/FacilityList.tsx @@ -0,0 +1,90 @@ +import { Button } from '@/Components/ui/button'; +import { Input } from '@/Components/ui/input'; +import { Skeleton } from '@/Components/ui/skeleton'; +import { Facility } from '@/types'; +import axios from 'axios'; +import { Search } from 'lucide-react'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { columns } from './Columns'; +import { DataTable } from './DataTable'; + +export default function FacilityList({ + onSelect, +}: { + onSelect: (facility: Facility) => void; +}) { + const [data, setData] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); + const inputRef = useRef(null); + + const getFacilities = useCallback((searchTerm?: string) => { + const getData = (): Promise => { + return axios + .get(route('facilities.search'), { + params: { + query: searchTerm, + with: [], + }, + }) + .then((response) => response.data); + }; + + setIsLoading(true); + + getData() + .then((facilities) => { + setData(facilities); + setIsLoading(false); + }) + .catch((error) => { + console.error('Error fetching facilities:', error); + setIsLoading(false); + }); + }, []); + + useEffect(() => { + if (!isLoading) { + inputRef.current?.focus(); + } + }, [isLoading]); + + useEffect(() => { + getFacilities(); + }, [getFacilities]); + + return ( +
+
+ setSearchTerm(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + getFacilities(searchTerm); + } + }} + /> + +
+ {isLoading ? ( + <> + + + ) : ( + <> + + + )} +
+ ); +} diff --git a/resources/js/Components/Shipper/ShipperList/Columns.tsx b/resources/js/Components/Shipper/ShipperList/Columns.tsx new file mode 100644 index 0000000..cbd655b --- /dev/null +++ b/resources/js/Components/Shipper/ShipperList/Columns.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { Shipper } from '@/types'; +import { ColumnDef } from '@tanstack/react-table'; + +export const columns: ColumnDef[] = [ + { + accessorKey: 'name', + header: 'Name', + cell: ({ row }) => { + return ( +
+ {row.original.name} +
+ ); + }, + }, +]; diff --git a/resources/js/Components/Shipper/ShipperList/DataTable.tsx b/resources/js/Components/Shipper/ShipperList/DataTable.tsx new file mode 100644 index 0000000..b27ce2a --- /dev/null +++ b/resources/js/Components/Shipper/ShipperList/DataTable.tsx @@ -0,0 +1,93 @@ +'use client'; + +import { + ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, +} from '@tanstack/react-table'; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/Components/ui/table'; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + onSelect: (shipper: TData) => void; +} + +export function DataTable({ + columns, + data, + onSelect, +}: DataTableProps) { + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + }); + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef + .header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + { + onSelect(row.original); + }} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+ ); +} diff --git a/resources/js/Components/Shipper/ShipperList/ShipperList.tsx b/resources/js/Components/Shipper/ShipperList/ShipperList.tsx new file mode 100644 index 0000000..370dbd8 --- /dev/null +++ b/resources/js/Components/Shipper/ShipperList/ShipperList.tsx @@ -0,0 +1,90 @@ +import { Button } from '@/Components/ui/button'; +import { Input } from '@/Components/ui/input'; +import { Skeleton } from '@/Components/ui/skeleton'; +import { Shipper } from '@/types'; +import axios from 'axios'; +import { Search } from 'lucide-react'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { columns } from './Columns'; +import { DataTable } from './DataTable'; + +export default function ShipperList({ + onSelect, +}: { + onSelect: (shipper: Shipper) => void; +}) { + const [data, setData] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); + const inputRef = useRef(null); + + const getShippers = useCallback((searchTerm?: string) => { + const getData = (): Promise => { + return axios + .get(route('shippers.search'), { + params: { + query: searchTerm, + with: [], + }, + }) + .then((response) => response.data); + }; + + setIsLoading(true); + + getData() + .then((shippers) => { + setData(shippers); + setIsLoading(false); + }) + .catch((error) => { + console.error('Error fetching shippers:', error); + setIsLoading(false); + }); + }, []); + + useEffect(() => { + if (!isLoading) { + inputRef.current?.focus(); + } + }, [isLoading]); + + useEffect(() => { + getShippers(); + }, [getShippers]); + + return ( +
+
+ setSearchTerm(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + getShippers(searchTerm); + } + }} + /> + +
+ {isLoading ? ( + <> + + + ) : ( + <> + + + )} +
+ ); +} diff --git a/resources/js/Pages/Carriers/Index.tsx b/resources/js/Pages/Carriers/Index.tsx new file mode 100644 index 0000000..a0741e7 --- /dev/null +++ b/resources/js/Pages/Carriers/Index.tsx @@ -0,0 +1,51 @@ +import CarrierList from '@/Components/Carriers/CarrierList/CarrierList'; +import { buttonVariants } from '@/Components/ui/button'; +import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; +import { Carrier } from '@/types'; +import { Head, Link, usePage } from '@inertiajs/react'; +import { useEffect, useState } from 'react'; +import CarrierDetails from './Partials/CarrierDetails'; + +export default function Index() { + const { carrier } = usePage().props; + const [selectedCarrier, setSelectedCarrier] = useState( + carrier as Carrier, + ); + + useEffect(() => { + if (selectedCarrier) { + const url = new URL(window.location.href); + url.searchParams.set('carrier_id', selectedCarrier.id.toString()); + window.history.pushState({}, '', url.toString()); + } else { + const url = new URL(window.location.href); + url.searchParams.delete('carrier_id'); + window.history.pushState({}, '', url.toString()); + } + }, [selectedCarrier]); + + return ( + + +
+ + Create Carrier + +
+
+ + +
+
+ ); +} diff --git a/resources/js/Pages/Carriers/Partials/CarrierDetails.tsx b/resources/js/Pages/Carriers/Partials/CarrierDetails.tsx new file mode 100644 index 0000000..66fb660 --- /dev/null +++ b/resources/js/Pages/Carriers/Partials/CarrierDetails.tsx @@ -0,0 +1,12 @@ +import { Card } from '@/Components/ui/card'; +import { Carrier } from '@/types'; + +export default function CarrierDetails({ carrier }: { carrier?: Carrier }) { + return ( + +
+

{carrier?.name}

+
+
+ ); +} diff --git a/resources/js/Pages/Facilities/Index.tsx b/resources/js/Pages/Facilities/Index.tsx new file mode 100644 index 0000000..069f6b8 --- /dev/null +++ b/resources/js/Pages/Facilities/Index.tsx @@ -0,0 +1,51 @@ +import FacilityList from '@/Components/Facilities/FacilityList/FacilityList'; +import { buttonVariants } from '@/Components/ui/button'; +import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; +import { Facility } from '@/types'; +import { Head, Link, usePage } from '@inertiajs/react'; +import { useEffect, useState } from 'react'; +import FacilityDetails from './Partials/FacilityDetails'; + +export default function Index() { + const { facility } = usePage().props; + const [selectedFacility, setSelectedFacility] = useState< + Facility | undefined + >(facility as Facility); + + useEffect(() => { + if (selectedFacility) { + const url = new URL(window.location.href); + url.searchParams.set('facility_id', selectedFacility.id.toString()); + window.history.pushState({}, '', url.toString()); + } else { + const url = new URL(window.location.href); + url.searchParams.delete('facility_id'); + window.history.pushState({}, '', url.toString()); + } + }, [selectedFacility]); + + return ( + + +
+ + Create Facility + +
+
+ + +
+
+ ); +} diff --git a/resources/js/Pages/Facilities/Partials/FacilityDetails.tsx b/resources/js/Pages/Facilities/Partials/FacilityDetails.tsx new file mode 100644 index 0000000..ab75ddd --- /dev/null +++ b/resources/js/Pages/Facilities/Partials/FacilityDetails.tsx @@ -0,0 +1,12 @@ +import { Card } from '@/Components/ui/card'; +import { Facility } from '@/types'; + +export default function FacilityDetails({ facility }: { facility?: Facility }) { + return ( + +
+

{facility?.name}

+
+
+ ); +} diff --git a/resources/js/Pages/Shippers/Index.tsx b/resources/js/Pages/Shippers/Index.tsx new file mode 100644 index 0000000..33574d3 --- /dev/null +++ b/resources/js/Pages/Shippers/Index.tsx @@ -0,0 +1,51 @@ +import ShipperList from '@/Components/Shipper/ShipperList/ShipperList'; +import { buttonVariants } from '@/Components/ui/button'; +import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; +import { Shipper } from '@/types'; +import { Head, Link, usePage } from '@inertiajs/react'; +import { useEffect, useState } from 'react'; +import ShipperDetails from './Partials/ShipperDetails'; + +export default function Index() { + const { shipper } = usePage().props; + const [selectedShipper, setSelectedShipper] = useState( + shipper as Shipper, + ); + + useEffect(() => { + if (selectedShipper) { + const url = new URL(window.location.href); + url.searchParams.set('shipper_id', selectedShipper.id.toString()); + window.history.pushState({}, '', url.toString()); + } else { + const url = new URL(window.location.href); + url.searchParams.delete('shipper_id'); + window.history.pushState({}, '', url.toString()); + } + }, [selectedShipper]); + + return ( + + +
+ + Create Shipper + +
+
+ + +
+
+ ); +} diff --git a/resources/js/Pages/Shippers/Partials/ShipperDetails.tsx b/resources/js/Pages/Shippers/Partials/ShipperDetails.tsx new file mode 100644 index 0000000..c55944c --- /dev/null +++ b/resources/js/Pages/Shippers/Partials/ShipperDetails.tsx @@ -0,0 +1,12 @@ +import { Card } from '@/Components/ui/card'; +import { Shipper } from '@/types'; + +export default function ShipperDetails({ shipper }: { shipper?: Shipper }) { + return ( + +
+

{shipper?.name}

+
+
+ ); +} diff --git a/routes/web.php b/routes/web.php index 2e029f2..0c557f1 100644 --- a/routes/web.php +++ b/routes/web.php @@ -73,6 +73,8 @@ Route::get('facilities/search', [FacilityController::class, 'search'])->name('facilities.search'); Route::post('facilities', CreateFacility::class)->name('facilities.store'); + Route::get('facilities', [FacilityController::class, 'index'])->name('facilities.index'); + Route::post('facilities', [FacilityController::class, 'store'])->name('facilities.store'); Route::get('carriers/search', [CarrierController::class, 'search'])->name('carriers.search'); Route::resource('carriers', CarrierController::class);