Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/inventory/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ class PublicCollectionItemSerializer(serializers.ModelSerializer):

current_location = LocationSerializer(read_only=True)
location_name = serializers.SerializerMethodField()
box_code = serializers.CharField(source="box.box_code", read_only=True)

class Meta:
model = CollectionItem
Expand All @@ -140,6 +141,7 @@ class Meta:
"is_on_floor",
"current_location",
"location_name",
"box_code",
"created_at",
"updated_at",
]
Expand All @@ -153,6 +155,7 @@ class Meta:
"working_condition",
"status",
"is_on_floor",
"box_code",
"current_location",
"location_name",
"created_at",
Expand Down
15 changes: 11 additions & 4 deletions frontend/src/actions/useItems.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { useQuery } from '@tanstack/react-query';
import { itemsApi } from '../api/items.api';
import { publicItemsApi, itemsApi } from '../api/items.api';
import type { ItemFilter } from '../lib/filters';

export const useItems = (filters?: ItemFilter) => {
export const usePublicItems = (filters?: ItemFilter) => {
return useQuery({
queryKey: ['items', 'list', filters],
queryFn: () => itemsApi.getAll(filters), // Use the updated `itemsApi.getAll`
queryKey: ['public-items', filters],
queryFn: () => publicItemsApi.getAll(filters),
});
};

export const useAdminItems = (filters?: ItemFilter) => {
return useQuery({
queryKey: ['admin-items', filters],
queryFn: () => itemsApi.getAll(filters),
});
};
94 changes: 86 additions & 8 deletions frontend/src/api/items.api.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,107 @@
// Collection Items API calls
import apiClient from './apiClient';
import type { PublicCollectionItem } from '../lib/types';
import type {
ItemType,
ItemStatus,
PublicCollectionItem,
AdminCollectionItem,
} from '../lib/types';

import type { ItemFilter } from '../lib/filters';

export interface CreateItemData {
item_code: string;
title: string;
platform: string;
description: string;
item_type: ItemType;
working_condition: boolean;
status: ItemStatus;
current_location: number;
is_public_visible: boolean;
is_on_floor: boolean;
box: number | null;
}

export type UpdateItemData = Partial<CreateItemData>;

export const itemsApi = {
// ADMIN ONLY

getAll: async (params?: ItemFilter): Promise<AdminCollectionItem[]> => {
const queryParams: Record<string, string> = {};

if (params?.platform) queryParams.platform = params.platform;
if (params?.is_on_floor !== undefined && params?.is_on_floor !== null) {
queryParams.is_on_floor = params.is_on_floor ? 'true' : 'false';
}
if (params?.search) queryParams.search = params.search;
if (params?.ordering) queryParams.ordering = params.ordering;
if (params?.item_type) queryParams.item_type = params.item_type;
if (params?.status) queryParams.status = params.status;
if (params?.location_type) queryParams.location_type = params.location_type;

queryParams.page_size = '10000';

const response = await apiClient.get('/inventory/items/', {
params: queryParams,
});

const data = response.data;

if (Array.isArray(data)) return data;
if (data?.results && Array.isArray(data.results)) return data.results;
return [];
},

getById: async (id: string | number): Promise<AdminCollectionItem> => {
const response = await apiClient.get(`/inventory/items/${id}/`);
return response.data;
Comment on lines +30 to +58
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

itemsApi.getAll() / getById() are typed to return AdminCollectionItem with a nested current_location object, but the backend /inventory/items/ endpoint uses CollectionItemViewSet which returns CollectionItemSerializer for list/retrieve (i.e., current_location and box are plain IDs, not nested objects). This will cause the UI to show Unknown/-- for location and makes the edit form default location blank. Options: (1) change the backend list/retrieve serializer to return nested current_location (or use/register the existing AdminCollectionItemViewSet that uses AdminCollectionItemSerializer), or (2) update the frontend types/parsing to match the ID-only payload and fetch location details separately.

Copilot uses AI. Check for mistakes.
},

create: async (data: CreateItemData): Promise<AdminCollectionItem> => {
const response = await apiClient.post('/inventory/items/', data);
return response.data;
},

partialUpdate: async (
id: string | number,
data: UpdateItemData
): Promise<AdminCollectionItem> => {
const response = await apiClient.patch(`/inventory/items/${id}/`, data);
return response.data;
},
};


export const publicItemsApi = {
getAll: async (params?: ItemFilter): Promise<PublicCollectionItem[]> => {
const queryParams: Record<string, string> = {};

if (params?.platform) queryParams.platform = params.platform;
if (params?.is_on_floor !== undefined && params?.is_on_floor !== null)
if (params?.is_on_floor !== undefined && params?.is_on_floor !== null) {
queryParams.is_on_floor = params.is_on_floor ? 'true' : 'false';
}
if (params?.search) queryParams.search = params.search;
if (params?.ordering) queryParams.ordering = params.ordering;
if (params?.item_type) queryParams.item_type = params.item_type;
if (params?.status) queryParams.status = params.status;
if (params?.location_type) queryParams.location_type = params.location_type;

// Fetch all items by setting a large page size

queryParams.page_size = '10000';

const response = await apiClient.get('/inventory/public/items/', { params: queryParams });
return response.data.results ?? response.data;
const response = await apiClient.get('/inventory/public/items/', {
params: queryParams,
});

const data = response.data;

if (Array.isArray(data)) return data;
if (data?.results && Array.isArray(data.results)) return data.results;
return [];
},

getById: async (id: string | number): Promise<PublicCollectionItem> => {
const response = await apiClient.get(`/inventory/public/items/${id}/`);
return response.data;
}
},
};
6 changes: 4 additions & 2 deletions frontend/src/components/common/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
radius?: "xs" | "sm" | "md" | "lg" | "xl"
icon?: keyof typeof iconMap
layout?: "default" | "stacked"
hideMobile?: boolean
}

export function Button({
Expand All @@ -32,19 +33,20 @@ export function Button({
layout = "default",
className,
type,
hideMobile = false,
children,
...props
}: ButtonProps) {

const resolvedType = type ?? "button"

const classes: string[] = [
"inline-flex items-center justify-center font-medium transition",
hideMobile ? "hidden md:inline-flex" : "inline-flex",
"items-center justify-center font-medium transition",
"outline-none",
"focus-visible:ring-2 focus-visible:ring-offset-2",
"disabled:opacity-50 disabled:pointer-events-none",
]

/* ---------- Radius ---------- */
if (radius) classes.push(`rounded-${radius}`)
else classes.push("rounded")
Expand Down
9 changes: 4 additions & 5 deletions frontend/src/components/items/CatalogueSearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const CatalogueSearchBar: React.FC<SearchBarProps> = ({ filters, setFilters }) =
const handler = setTimeout(() => {
setFilters((prev) => ({
...prev,
search: searchValue.trim() || null
search: searchValue.trim() || undefined
}));
}, 300);

Expand All @@ -24,13 +24,12 @@ const CatalogueSearchBar: React.FC<SearchBarProps> = ({ filters, setFilters }) =


const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value.trim();
setSearchValue(value);
setSearchValue(e.target.value);
};

const handlePlatformChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value.trim();
setFilters(prev => ({ ...prev, platform: value || null }));
setFilters(prev => ({ ...prev, platform: value || undefined }));
};

const handleFloorChange = (e: React.ChangeEvent<HTMLInputElement>) => {
Expand All @@ -42,7 +41,7 @@ const CatalogueSearchBar: React.FC<SearchBarProps> = ({ filters, setFilters }) =

const handleOrderingChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value.trim();
setFilters(prev => ({ ...prev, ordering: value || null }));
setFilters(prev => ({ ...prev, ordering: value || undefined }));
};


Expand Down
10 changes: 10 additions & 0 deletions frontend/src/components/items/DetailRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
function DetailRow({ label, value }: { label: string; value: string }) {
return (
<div className="flex items-center justify-between px-4 py-3">
<span className="text-xs font-semibold text-secondary tracking-wide">{label}</span>
<span className="text-sm text-primary">{value}</span>
</div>
)
}

export default DetailRow;
Loading
Loading