Skip to content
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
2 changes: 1 addition & 1 deletion frontend/src/actions/useItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ import type { ItemFilter } from '../lib/filters';
export const useItems = (filters?: ItemFilter) => {
return useQuery({
queryKey: ['items', 'list', filters],
queryFn: () => itemsApi.getAll(filters), // Use the updated `itemsApi.getAll`
queryFn: () => itemsApi.getAll(filters),
});
};
71 changes: 60 additions & 11 deletions frontend/src/api/items.api.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,78 @@
// Collection Items API calls
import apiClient from './apiClient';
import type { PublicCollectionItem } from '../lib/types';
import type {
ItemType,
ItemStatus,
CollectionItem,
} 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 = {
getAll: async (params?: ItemFilter): Promise<PublicCollectionItem[]> => {
getAll: async (params?: ItemFilter): Promise<CollectionItem[]> => {
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}/`);
getById: async (id: string | number): Promise<CollectionItem> => {
const response = await apiClient.get(`/inventory/items/${id}/`);
return response.data;
}
},

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

update: async (
id: string | number,
data: CreateItemData
): Promise<CollectionItem> => {
const response = await apiClient.put(`/inventory/items/${id}/`, data);
return response.data;
},

partialUpdate: async (
id: string | number,
data: UpdateItemData
): Promise<CollectionItem> => {
const response = await apiClient.patch(`/inventory/items/${id}/`, data);
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;
4 changes: 2 additions & 2 deletions frontend/src/components/items/ItemCard.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Item card component
import type { PublicCollectionItem } from "../../lib/types"
import type { CollectionItem } from "../../lib/types"
import './ItemCard.css';

interface ItemCardProps {
item: PublicCollectionItem;
item: CollectionItem;
}

const ItemCard: React.FC<ItemCardProps> = ({ item }) => {
Expand Down
28 changes: 28 additions & 0 deletions frontend/src/components/items/ItemDetailCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import DetailRow from "./DetailRow"
type ItemDetailsProps = {
item: {
madeId: string
platform: string
location: string
}
onUpdateLocation?: () => void
}

export function ItemDetailsCard({ item }: ItemDetailsProps) {
return (
<div className="w-full rounded-xl border border-border bg-background overflow-hidden">

{/* Header */}
<div className="px-4 py-3 border-b border-border">
<h2 className="text-sm font-semibold text-primary">Item Details</h2>
</div>

{/* Rows */}
<div className="divide-y divide-border">
<DetailRow label="MADE ID" value={item.madeId} />
<DetailRow label="Platform" value={item.platform} />
<DetailRow label="Location / Box" value={item.location} />
</div>
</div>
)
}
4 changes: 2 additions & 2 deletions frontend/src/components/items/ItemList.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// Item list component
import type { PublicCollectionItem } from "../../lib/types";
import type { CollectionItem } from "../../lib/types";
import ItemCard from "./ItemCard";
import './ItemList.css';

interface ItemListProps {
items: PublicCollectionItem[];
items: CollectionItem[];
}

const ItemList: React.FC<ItemListProps> = ({items}) => {
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/components/layout/Header.css
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,9 @@
.header-link-logout:hover {
color: var(--color-primary);
}

@media (max-width: 767px) {
.header-nav {
display: none;
}
}
4 changes: 4 additions & 0 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
/* Colors - Tan/Beige Accent */
--color-tan-bg: #fef7ed;
--color-tan-border: #fed7aa;

/* Colors - Light Blue Accent */
--color-light-blue-bg: #EFF6FF;
--color-light-blue-border: #BFDBFE;

/* Colors - Status */
--color-success: #22c55e;
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/lib/filters.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export interface ItemFilter {
search?: string | null;
platform?: string | null;
platform?: string;
is_on_floor?: boolean | null;
ordering?: string | null;
item_type?: 'SOFTWARE' | 'HARDWARE' | null;
status?: 'AVAILABLE' | 'IN_TRANSIT' | 'CHECKED_OUT' | 'MAINTENANCE' | null;
location_type?: 'FLOOR' | 'STORAGE' | 'EVENT' | 'OTHER' | null;
search?: string;
ordering?: string;
item_type?: string;
status?: string;
location_type?: string;
}
57 changes: 18 additions & 39 deletions frontend/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,10 @@ export interface VolunteerApplicationInput {
motivation_text: string
}

// TODO: place holder
export interface CollectionItem {
id: number;
name: string;
description: string;
}
export type ItemType = 'SOFTWARE' | 'HARDWARE' | 'NON_ELECTRONIC';
export type ItemStatus = 'AVAILABLE' | 'IN_TRANSIT' | 'CHECKED_OUT' | 'MAINTENANCE';
export type ConditionType = 'EXCELLENT' | 'GOOD' | 'FAIR' | 'POOR';
export type CompletenessType = 'YES' | 'NO' | 'UNKNOWN';

export interface LocationInfo {
id: number;
Expand All @@ -41,46 +39,27 @@ export interface LocationInfo {
description?: string;
}

export type ItemType = 'SOFTWARE' | 'HARDWARE' | 'NON_ELECTRONIC';
export type ConditionType = 'EXCELLENT' | 'GOOD' | 'FAIR' | 'POOR';
export type CompletenessType = 'YES' | 'NO' | 'UNKNOWN';
export interface BoxSummary {
id: number;
box_code?: string;
label?: string;
description?: string;
location?: number;
}

export interface PublicCollectionItem {
export interface CollectionItem {
id: number;
item_code: string;
title: string;
platform: string;
description: string;
item_type?: ItemType;
condition?: ConditionType;
is_complete?: CompletenessType;
is_functional?: CompletenessType;
date_of_entry?: string;
working_condition?: boolean;
status?: 'AVAILABLE' | 'IN_TRANSIT' | 'CHECKED_OUT' | 'MAINTENANCE';
item_type: ItemType;
working_condition: boolean;
status: ItemStatus;
current_location: LocationInfo | number | null;
is_public_visible: boolean;
is_on_floor: boolean;
location_name?: string;
current_location?: LocationInfo;
box_code?: string;
created_at?: string;
updated_at?: string;
// Software-specific fields
creator_publisher?: string;
release_year?: string;
version_edition?: string;
media_type?: string;
// Hardware-specific fields
manufacturer?: string;
model_number?: string;
year_manufactured?: string;
serial_number?: string;
hardware_type?: string;
// Non-Electronic-specific fields
item_subtype?: string;
date_published?: string;
publisher?: string;
volume_number?: string;
isbn_catalogue_number?: string;
box: BoxSummary | number | null;
}

// TODO: place holder
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/pages/public/CataloguePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import './CataloguePage.css';
const CataloguePage: React.FC = () => {

const [filters, setFilters] = useState<ItemFilter>({
search: null,
platform: null,
search: undefined,
platform: undefined,
is_on_floor: null,
ordering: null,
ordering: undefined,
});

const { data: items = [], isLoading, isError } = useItems(filters);
Expand Down
Loading