feat: add location crud
All checks were successful
Deploy to Production / deploy (push) Successful in 1m2s

This commit is contained in:
魏风
2026-03-12 19:47:41 +08:00
parent f0a0667b84
commit ef93d4e5c2
19 changed files with 1278 additions and 8 deletions

View File

@@ -196,6 +196,131 @@ export const ItemsPublicSchema = {
title: 'ItemsPublic'
} as const;
export const LocationCreateSchema = {
properties: {
title: {
type: 'string',
maxLength: 255,
minLength: 1,
title: 'Title'
},
description: {
anyOf: [
{
type: 'string',
maxLength: 255
},
{
type: 'null'
}
],
title: 'Description'
}
},
type: 'object',
required: ['title'],
title: 'LocationCreate'
} as const;
export const LocationPublicSchema = {
properties: {
title: {
type: 'string',
maxLength: 255,
minLength: 1,
title: 'Title'
},
description: {
anyOf: [
{
type: 'string',
maxLength: 255
},
{
type: 'null'
}
],
title: 'Description'
},
id: {
type: 'string',
format: 'uuid',
title: 'Id'
},
owner_id: {
type: 'string',
format: 'uuid',
title: 'Owner Id'
},
created_at: {
anyOf: [
{
type: 'string',
format: 'date-time'
},
{
type: 'null'
}
],
title: 'Created At'
}
},
type: 'object',
required: ['title', 'id', 'owner_id'],
title: 'LocationPublic'
} as const;
export const LocationUpdateSchema = {
properties: {
title: {
anyOf: [
{
type: 'string',
maxLength: 255,
minLength: 1
},
{
type: 'null'
}
],
title: 'Title'
},
description: {
anyOf: [
{
type: 'string',
maxLength: 255
},
{
type: 'null'
}
],
title: 'Description'
}
},
type: 'object',
title: 'LocationUpdate'
} as const;
export const LocationsPublicSchema = {
properties: {
data: {
items: {
'$ref': '#/components/schemas/LocationPublic'
},
type: 'array',
title: 'Data'
},
count: {
type: 'integer',
title: 'Count'
}
},
type: 'object',
required: ['data', 'count'],
title: 'LocationsPublic'
} as const;
export const MessageSchema = {
properties: {
message: {

View File

@@ -3,7 +3,7 @@
import type { CancelablePromise } from './core/CancelablePromise';
import { OpenAPI } from './core/OpenAPI';
import { request as __request } from './core/request';
import type { ItemsReadItemsData, ItemsReadItemsResponse, ItemsCreateItemData, ItemsCreateItemResponse, ItemsReadItemData, ItemsReadItemResponse, ItemsUpdateItemData, ItemsUpdateItemResponse, ItemsDeleteItemData, ItemsDeleteItemResponse, LoginLoginAccessTokenData, LoginLoginAccessTokenResponse, LoginTestTokenResponse, LoginRecoverPasswordData, LoginRecoverPasswordResponse, LoginResetPasswordData, LoginResetPasswordResponse, LoginRecoverPasswordHtmlContentData, LoginRecoverPasswordHtmlContentResponse, PrivateCreateUserData, PrivateCreateUserResponse, UsersReadUsersData, UsersReadUsersResponse, UsersCreateUserData, UsersCreateUserResponse, UsersReadUserMeResponse, UsersDeleteUserMeResponse, UsersUpdateUserMeData, UsersUpdateUserMeResponse, UsersUpdatePasswordMeData, UsersUpdatePasswordMeResponse, UsersRegisterUserData, UsersRegisterUserResponse, UsersReadUserByIdData, UsersReadUserByIdResponse, UsersUpdateUserData, UsersUpdateUserResponse, UsersDeleteUserData, UsersDeleteUserResponse, UtilsTestEmailData, UtilsTestEmailResponse, UtilsHealthCheckResponse } from './types.gen';
import type { ItemsReadItemsData, ItemsReadItemsResponse, ItemsCreateItemData, ItemsCreateItemResponse, ItemsReadItemData, ItemsReadItemResponse, ItemsUpdateItemData, ItemsUpdateItemResponse, ItemsDeleteItemData, ItemsDeleteItemResponse, LocationsReadLocationsData, LocationsReadLocationsResponse, LocationsCreateLocationData, LocationsCreateLocationResponse, LocationsReadLocationData, LocationsReadLocationResponse, LocationsUpdateLocationData, LocationsUpdateLocationResponse, LocationsDeleteLocationData, LocationsDeleteLocationResponse, LoginLoginAccessTokenData, LoginLoginAccessTokenResponse, LoginTestTokenResponse, LoginRecoverPasswordData, LoginRecoverPasswordResponse, LoginResetPasswordData, LoginResetPasswordResponse, LoginRecoverPasswordHtmlContentData, LoginRecoverPasswordHtmlContentResponse, PrivateCreateUserData, PrivateCreateUserResponse, UsersReadUsersData, UsersReadUsersResponse, UsersCreateUserData, UsersCreateUserResponse, UsersReadUserMeResponse, UsersDeleteUserMeResponse, UsersUpdateUserMeData, UsersUpdateUserMeResponse, UsersUpdatePasswordMeData, UsersUpdatePasswordMeResponse, UsersRegisterUserData, UsersRegisterUserResponse, UsersReadUserByIdData, UsersReadUserByIdResponse, UsersUpdateUserData, UsersUpdateUserResponse, UsersDeleteUserData, UsersDeleteUserResponse, UtilsTestEmailData, UtilsTestEmailResponse, UtilsHealthCheckResponse } from './types.gen';
export class ItemsService {
/**
@@ -116,6 +116,117 @@ export class ItemsService {
}
}
export class LocationsService {
/**
* Read Locations
* Retrieve locations.
* @param data The data for the request.
* @param data.skip
* @param data.limit
* @returns LocationsPublic Successful Response
* @throws ApiError
*/
public static readLocations(data: LocationsReadLocationsData = {}): CancelablePromise<LocationsReadLocationsResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/locations/',
query: {
skip: data.skip,
limit: data.limit
},
errors: {
422: 'Validation Error'
}
});
}
/**
* Create Location
* Create new location.
* @param data The data for the request.
* @param data.requestBody
* @returns LocationPublic Successful Response
* @throws ApiError
*/
public static createLocation(data: LocationsCreateLocationData): CancelablePromise<LocationsCreateLocationResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/v1/locations/',
body: data.requestBody,
mediaType: 'application/json',
errors: {
422: 'Validation Error'
}
});
}
/**
* Read Location
* Get location by ID.
* @param data The data for the request.
* @param data.id
* @returns LocationPublic Successful Response
* @throws ApiError
*/
public static readLocation(data: LocationsReadLocationData): CancelablePromise<LocationsReadLocationResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/locations/{id}',
path: {
id: data.id
},
errors: {
422: 'Validation Error'
}
});
}
/**
* Update Location
* Update a location.
* @param data The data for the request.
* @param data.id
* @param data.requestBody
* @returns LocationPublic Successful Response
* @throws ApiError
*/
public static updateLocation(data: LocationsUpdateLocationData): CancelablePromise<LocationsUpdateLocationResponse> {
return __request(OpenAPI, {
method: 'PUT',
url: '/api/v1/locations/{id}',
path: {
id: data.id
},
body: data.requestBody,
mediaType: 'application/json',
errors: {
422: 'Validation Error'
}
});
}
/**
* Delete Location
* Delete a location.
* @param data The data for the request.
* @param data.id
* @returns Message Successful Response
* @throws ApiError
*/
public static deleteLocation(data: LocationsDeleteLocationData): CancelablePromise<LocationsDeleteLocationResponse> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/api/v1/locations/{id}',
path: {
id: data.id
},
errors: {
422: 'Validation Error'
}
});
}
}
export class LoginService {
/**
* Login Access Token

View File

@@ -36,6 +36,29 @@ export type ItemUpdate = {
description?: (string | null);
};
export type LocationCreate = {
title: string;
description?: (string | null);
};
export type LocationPublic = {
title: string;
description?: (string | null);
id: string;
owner_id: string;
created_at?: (string | null);
};
export type LocationsPublic = {
data: Array<LocationPublic>;
count: number;
};
export type LocationUpdate = {
title?: (string | null);
description?: (string | null);
};
export type Message = {
message: string;
};
@@ -145,6 +168,38 @@ export type ItemsDeleteItemData = {
export type ItemsDeleteItemResponse = (Message);
export type LocationsReadLocationsData = {
limit?: number;
skip?: number;
};
export type LocationsReadLocationsResponse = (LocationsPublic);
export type LocationsCreateLocationData = {
requestBody: LocationCreate;
};
export type LocationsCreateLocationResponse = (LocationPublic);
export type LocationsReadLocationData = {
id: string;
};
export type LocationsReadLocationResponse = (LocationPublic);
export type LocationsUpdateLocationData = {
id: string;
requestBody: LocationUpdate;
};
export type LocationsUpdateLocationResponse = (LocationPublic);
export type LocationsDeleteLocationData = {
id: string;
};
export type LocationsDeleteLocationResponse = (Message);
export type LoginLoginAccessTokenData = {
formData: Body_login_login_access_token;
};

View File

@@ -0,0 +1,144 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { Plus } from "lucide-react"
import { useState } from "react"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { type LocationCreate, LocationsService } from "@/client"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { LoadingButton } from "@/components/ui/loading-button"
import useCustomToast from "@/hooks/useCustomToast"
import { handleError } from "@/utils"
const formSchema = z.object({
title: z.string().min(1, { message: "Title is required" }),
description: z.string().optional(),
})
type FormData = z.infer<typeof formSchema>
const AddLocation = () => {
const [isOpen, setIsOpen] = useState(false)
const queryClient = useQueryClient()
const { showSuccessToast, showErrorToast } = useCustomToast()
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
mode: "onBlur",
criteriaMode: "all",
defaultValues: {
title: "",
description: "",
},
})
const mutation = useMutation({
mutationFn: (data: LocationCreate) =>
LocationsService.createLocation({ requestBody: data }),
onSuccess: () => {
showSuccessToast("Location created successfully")
form.reset()
setIsOpen(false)
},
onError: handleError.bind(showErrorToast),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["locations"] })
},
})
const onSubmit = (data: FormData) => {
mutation.mutate(data)
}
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button className="my-4">
<Plus className="mr-2" />
Add Location
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Add Location</DialogTitle>
<DialogDescription>
Fill in the details to add a new location.
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="grid gap-4 py-4">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>
Title <span className="text-destructive">*</span>
</FormLabel>
<FormControl>
<Input
placeholder="Title"
type="text"
{...field}
required
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Input placeholder="Description" type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline" disabled={mutation.isPending}>
Cancel
</Button>
</DialogClose>
<LoadingButton type="submit" loading={mutation.isPending}>
Save
</LoadingButton>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
)
}
export default AddLocation

View File

@@ -0,0 +1,94 @@
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { Trash2 } from "lucide-react"
import { useState } from "react"
import { useForm } from "react-hook-form"
import { LocationsService } from "@/client"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { DropdownMenuItem } from "@/components/ui/dropdown-menu"
import { LoadingButton } from "@/components/ui/loading-button"
import useCustomToast from "@/hooks/useCustomToast"
import { handleError } from "@/utils"
interface DeleteLocationProps {
id: string
onSuccess: () => void
}
const DeleteLocation = ({ id, onSuccess }: DeleteLocationProps) => {
const [isOpen, setIsOpen] = useState(false)
const queryClient = useQueryClient()
const { showSuccessToast, showErrorToast } = useCustomToast()
const { handleSubmit } = useForm()
const deleteLocation = async (id: string) => {
await LocationsService.deleteLocation({ id: id })
}
const mutation = useMutation({
mutationFn: deleteLocation,
onSuccess: () => {
showSuccessToast("The location was deleted successfully")
setIsOpen(false)
onSuccess()
},
onError: handleError.bind(showErrorToast),
onSettled: () => {
queryClient.invalidateQueries()
},
})
const onSubmit = async () => {
mutation.mutate(id)
}
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuItem
variant="destructive"
onSelect={(e) => e.preventDefault()}
onClick={() => setIsOpen(true)}
>
<Trash2 />
Delete Location
</DropdownMenuItem>
<DialogContent className="sm:max-w-md">
<form onSubmit={handleSubmit(onSubmit)}>
<DialogHeader>
<DialogTitle>Delete Location</DialogTitle>
<DialogDescription>
This location will be permanently deleted. Are you sure? You will not
be able to undo this action.
</DialogDescription>
</DialogHeader>
<DialogFooter className="mt-4">
<DialogClose asChild>
<Button variant="outline" disabled={mutation.isPending}>
Cancel
</Button>
</DialogClose>
<LoadingButton
variant="destructive"
type="submit"
loading={mutation.isPending}
>
Delete
</LoadingButton>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}
export default DeleteLocation

View File

@@ -0,0 +1,145 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { Pencil } from "lucide-react"
import { useState } from "react"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { type LocationPublic, LocationsService } from "@/client"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { DropdownMenuItem } from "@/components/ui/dropdown-menu"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { LoadingButton } from "@/components/ui/loading-button"
import useCustomToast from "@/hooks/useCustomToast"
import { handleError } from "@/utils"
const formSchema = z.object({
title: z.string().min(1, { message: "Title is required" }),
description: z.string().optional(),
})
type FormData = z.infer<typeof formSchema>
interface EditLocationProps {
location: LocationPublic
onSuccess: () => void
}
const EditLocation = ({ location, onSuccess }: EditLocationProps) => {
const [isOpen, setIsOpen] = useState(false)
const queryClient = useQueryClient()
const { showSuccessToast, showErrorToast } = useCustomToast()
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
mode: "onBlur",
criteriaMode: "all",
defaultValues: {
title: location.title,
description: location.description ?? undefined,
},
})
const mutation = useMutation({
mutationFn: (data: FormData) =>
LocationsService.updateLocation({ id: location.id, requestBody: data }),
onSuccess: () => {
showSuccessToast("Location updated successfully")
setIsOpen(false)
onSuccess()
},
onError: handleError.bind(showErrorToast),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["locations"] })
},
})
const onSubmit = (data: FormData) => {
mutation.mutate(data)
}
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
onClick={() => setIsOpen(true)}
>
<Pencil />
Edit Location
</DropdownMenuItem>
<DialogContent className="sm:max-w-md">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<DialogHeader>
<DialogTitle>Edit Location</DialogTitle>
<DialogDescription>
Update the location details below.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>
Title <span className="text-destructive">*</span>
</FormLabel>
<FormControl>
<Input placeholder="Title" type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Input placeholder="Description" type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline" disabled={mutation.isPending}>
Cancel
</Button>
</DialogClose>
<LoadingButton type="submit" loading={mutation.isPending}>
Save
</LoadingButton>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
)
}
export default EditLocation

View File

@@ -0,0 +1,34 @@
import { EllipsisVertical } from "lucide-react"
import { useState } from "react"
import type { LocationPublic } from "@/client"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import DeleteLocation from "../Locations/DeleteLocation"
import EditLocation from "../Locations/EditLocation"
interface LocationActionsMenuProps {
location: LocationPublic
}
export const LocationActionsMenu = ({ location }: LocationActionsMenuProps) => {
const [open, setOpen] = useState(false)
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<EllipsisVertical />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<EditLocation location={location} onSuccess={() => setOpen(false)} />
<DeleteLocation id={location.id} onSuccess={() => setOpen(false)} />
</DropdownMenuContent>
</DropdownMenu>
)
}

View File

@@ -0,0 +1,73 @@
import type { ColumnDef } from "@tanstack/react-table"
import { Check, Copy } from "lucide-react"
import type { LocationPublic } from "@/client"
import { Button } from "@/components/ui/button"
import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"
import { cn } from "@/lib/utils"
import { LocationActionsMenu } from "./LocationActionsMenu"
function CopyId({ id }: { id: string }) {
const [copiedText, copy] = useCopyToClipboard()
const isCopied = copiedText === id
return (
<div className="flex items-center gap-1.5 group">
<span className="font-mono text-xs text-muted-foreground">{id}</span>
<Button
variant="ghost"
size="icon"
className="size-6 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={() => copy(id)}
>
{isCopied ? (
<Check className="size-3 text-green-500" />
) : (
<Copy className="size-3" />
)}
<span className="sr-only">Copy ID</span>
</Button>
</div>
)
}
export const columns: ColumnDef<LocationPublic>[] = [
{
accessorKey: "id",
header: "ID",
cell: ({ row }) => <CopyId id={row.original.id} />,
},
{
accessorKey: "title",
header: "Title",
cell: ({ row }) => (
<span className="font-medium">{row.original.title}</span>
),
},
{
accessorKey: "description",
header: "Description",
cell: ({ row }) => {
const description = row.original.description
return (
<span
className={cn(
"max-w-xs truncate block text-muted-foreground",
!description && "italic",
)}
>
{description || "No description"}
</span>
)
},
},
{
id: "actions",
header: () => <span className="sr-only">Actions</span>,
cell: ({ row }) => (
<div className="flex justify-end">
<LocationActionsMenu location={row.original} />
</div>
),
},
]

View File

@@ -1,4 +1,4 @@
import { Briefcase, Home, Users } from "lucide-react"
import { Briefcase, Home, MapPin, Users } from "lucide-react"
import { SidebarAppearance } from "@/components/Common/Appearance"
import { Logo } from "@/components/Common/Logo"
@@ -14,6 +14,7 @@ import { User } from "./User"
const baseItems: Item[] = [
{ icon: Home, title: "Dashboard", path: "/" },
{ icon: MapPin, title: "Locations", path: "/locations" },
{ icon: Briefcase, title: "Items", path: "/items" },
]

View File

@@ -16,6 +16,7 @@ import { Route as LoginRouteImport } from './routes/login'
import { Route as LayoutRouteImport } from './routes/_layout'
import { Route as LayoutIndexRouteImport } from './routes/_layout/index'
import { Route as LayoutSettingsRouteImport } from './routes/_layout/settings'
import { Route as LayoutLocationsRouteImport } from './routes/_layout/locations'
import { Route as LayoutItemsRouteImport } from './routes/_layout/items'
import { Route as LayoutAdminRouteImport } from './routes/_layout/admin'
@@ -53,6 +54,11 @@ const LayoutSettingsRoute = LayoutSettingsRouteImport.update({
path: '/settings',
getParentRoute: () => LayoutRoute,
} as any)
const LayoutLocationsRoute = LayoutLocationsRouteImport.update({
id: '/locations',
path: '/locations',
getParentRoute: () => LayoutRoute,
} as any)
const LayoutItemsRoute = LayoutItemsRouteImport.update({
id: '/items',
path: '/items',
@@ -65,14 +71,15 @@ const LayoutAdminRoute = LayoutAdminRouteImport.update({
} as any)
export interface FileRoutesByFullPath {
'/': typeof LayoutIndexRoute
'/login': typeof LoginRoute
'/recover-password': typeof RecoverPasswordRoute
'/reset-password': typeof ResetPasswordRoute
'/signup': typeof SignupRoute
'/admin': typeof LayoutAdminRoute
'/items': typeof LayoutItemsRoute
'/locations': typeof LayoutLocationsRoute
'/settings': typeof LayoutSettingsRoute
'/': typeof LayoutIndexRoute
}
export interface FileRoutesByTo {
'/login': typeof LoginRoute
@@ -81,6 +88,7 @@ export interface FileRoutesByTo {
'/signup': typeof SignupRoute
'/admin': typeof LayoutAdminRoute
'/items': typeof LayoutItemsRoute
'/locations': typeof LayoutLocationsRoute
'/settings': typeof LayoutSettingsRoute
'/': typeof LayoutIndexRoute
}
@@ -93,20 +101,22 @@ export interface FileRoutesById {
'/signup': typeof SignupRoute
'/_layout/admin': typeof LayoutAdminRoute
'/_layout/items': typeof LayoutItemsRoute
'/_layout/locations': typeof LayoutLocationsRoute
'/_layout/settings': typeof LayoutSettingsRoute
'/_layout/': typeof LayoutIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
| '/login'
| '/recover-password'
| '/reset-password'
| '/signup'
| '/admin'
| '/items'
| '/locations'
| '/settings'
| '/'
fileRoutesByTo: FileRoutesByTo
to:
| '/login'
@@ -115,6 +125,7 @@ export interface FileRouteTypes {
| '/signup'
| '/admin'
| '/items'
| '/locations'
| '/settings'
| '/'
id:
@@ -126,6 +137,7 @@ export interface FileRouteTypes {
| '/signup'
| '/_layout/admin'
| '/_layout/items'
| '/_layout/locations'
| '/_layout/settings'
| '/_layout/'
fileRoutesById: FileRoutesById
@@ -171,7 +183,7 @@ declare module '@tanstack/react-router' {
'/_layout': {
id: '/_layout'
path: ''
fullPath: ''
fullPath: '/'
preLoaderRoute: typeof LayoutRouteImport
parentRoute: typeof rootRouteImport
}
@@ -189,6 +201,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof LayoutSettingsRouteImport
parentRoute: typeof LayoutRoute
}
'/_layout/locations': {
id: '/_layout/locations'
path: '/locations'
fullPath: '/locations'
preLoaderRoute: typeof LayoutLocationsRouteImport
parentRoute: typeof LayoutRoute
}
'/_layout/items': {
id: '/_layout/items'
path: '/items'
@@ -209,6 +228,7 @@ declare module '@tanstack/react-router' {
interface LayoutRouteChildren {
LayoutAdminRoute: typeof LayoutAdminRoute
LayoutItemsRoute: typeof LayoutItemsRoute
LayoutLocationsRoute: typeof LayoutLocationsRoute
LayoutSettingsRoute: typeof LayoutSettingsRoute
LayoutIndexRoute: typeof LayoutIndexRoute
}
@@ -216,6 +236,7 @@ interface LayoutRouteChildren {
const LayoutRouteChildren: LayoutRouteChildren = {
LayoutAdminRoute: LayoutAdminRoute,
LayoutItemsRoute: LayoutItemsRoute,
LayoutLocationsRoute: LayoutLocationsRoute,
LayoutSettingsRoute: LayoutSettingsRoute,
LayoutIndexRoute: LayoutIndexRoute,
}

View File

@@ -0,0 +1,69 @@
import { useSuspenseQuery } from "@tanstack/react-query"
import { createFileRoute } from "@tanstack/react-router"
import { Search } from "lucide-react"
import { Suspense } from "react"
import { LocationsService } from "@/client"
import { DataTable } from "@/components/Common/DataTable"
import AddLocation from "@/components/Locations/AddLocation"
import { columns } from "@/components/Locations/columns"
import PendingItems from "@/components/Pending/PendingItems"
function getLocationsQueryOptions() {
return {
queryFn: () => LocationsService.readLocations({ skip: 0, limit: 100 }),
queryKey: ["locations"],
}
}
export const Route = createFileRoute("/_layout/locations")({
component: Locations,
head: () => ({
meta: [
{
title: "Locations - FastAPI Template",
},
],
}),
})
function LocationsTableContent() {
const { data: locations } = useSuspenseQuery(getLocationsQueryOptions())
if (locations.data.length === 0) {
return (
<div className="flex flex-col items-center justify-center text-center py-12">
<div className="rounded-full bg-muted p-4 mb-4">
<Search className="h-8 w-8 text-muted-foreground" />
</div>
<h3 className="text-lg font-semibold">You don't have any locations yet</h3>
<p className="text-muted-foreground">Add a new location to get started</p>
</div>
)
}
return <DataTable columns={columns} data={locations.data} />
}
function LocationsTable() {
return (
<Suspense fallback={<PendingItems />}>
<LocationsTableContent />
</Suspense>
)
}
function Locations() {
return (
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold tracking-tight">Locations</h1>
<p className="text-muted-foreground">Create and manage your locations</p>
</div>
<AddLocation />
</div>
<LocationsTable />
</div>
)
}