Skip to content

Commit 724219f

Browse files
authored
Merge pull request #330 from con2/CON2-223-frontend-add-category-CRUD
Con2 223 frontend add category crud
2 parents 0663517 + 5be4987 commit 724219f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1342
-175
lines changed

backend/src/modules/categories/categories.controller.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export class CategoriesController {
2727

2828
// Create New Category
2929
@Post()
30-
@Roles(["tenant_admin", "storage_manager"])
30+
@Roles(["tenant_admin"])
3131
async createCategory(
3232
@Req() req: AuthRequest,
3333
@Body() newCategory: CreateCategoryDto,
@@ -38,7 +38,7 @@ export class CategoriesController {
3838

3939
// Update a Category
4040
@Patch(":id")
41-
@Roles(["tenant_admin", "storage_manager"])
41+
@Roles(["tenant_admin"])
4242
async updateCategory(
4343
@Req() req: AuthRequest,
4444
@Param("id") id: string,
@@ -54,7 +54,7 @@ export class CategoriesController {
5454

5555
// Delete a Category
5656
@Delete(":id")
57-
@Roles(["tenant_admin", "storage_manager"])
57+
@Roles(["tenant_admin"])
5858
async deleteCategory(@Req() req: AuthRequest, @Param("id") id: string) {
5959
const { supabase } = req;
6060
return await this.categoryService.deleteCategory({ supabase, id });

backend/src/modules/categories/categories.service.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
GetParamsDto,
77
UpdateParamsDto,
88
} from "./dto/params.dto";
9-
import { getPaginationRange } from "@src/utils/pagination";
9+
import { getPaginationMeta, getPaginationRange } from "@src/utils/pagination";
1010
import { handleSupabaseError } from "@src/utils/handleError.utils";
1111

1212
@Injectable()
@@ -20,15 +20,27 @@ export class CategoriesService {
2020
*/
2121
async getCategories(params: GetParamsDto) {
2222
const supabase = this.supabaseService.getAnonClient();
23-
const { page, limit } = params;
23+
const { page, limit, search, asc, order } = params;
2424
const { from, to } = getPaginationRange(page, limit);
2525

26-
const result = await supabase
27-
.from("categories")
26+
const query = supabase
27+
.from("view_category_details")
2828
.select("*", { count: "exact" })
2929
.range(from, to);
3030

31-
return result;
31+
const VALID_ORDERS = ["created_at", "assigned_to"];
32+
if (search)
33+
query.or(
34+
`translations->>en.ilike.%${search}%,translations->>fi.ilike.%${search}%`,
35+
);
36+
37+
query.order(VALID_ORDERS.includes(order) ? order : "created_at", {
38+
ascending: asc,
39+
});
40+
41+
const result = await query;
42+
const pagination = getPaginationMeta(result.count, page, limit);
43+
return { ...result, metadata: pagination };
3244
}
3345

3446
/**
Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,44 @@
1-
import { IsNotEmpty, IsOptional } from "class-validator";
1+
import {
2+
IsNotEmpty,
3+
IsOptional,
4+
IsUUID,
5+
ValidateNested,
6+
IsString,
7+
} from "class-validator";
8+
import { Type } from "class-transformer";
9+
10+
class CategoryTranslationsDto {
11+
@IsString()
12+
@IsNotEmpty()
13+
en: string;
14+
15+
@IsString()
16+
@IsNotEmpty()
17+
fi: string;
18+
}
219

320
export class CreateCategoryDto {
21+
@IsUUID()
22+
id: string;
23+
24+
@ValidateNested()
25+
@Type(() => CategoryTranslationsDto)
26+
translations: CategoryTranslationsDto;
27+
428
@IsOptional()
5-
parent_name: string;
6-
@IsNotEmpty()
7-
name: string;
29+
@IsUUID()
30+
parent_id?: string | null;
831
}
932

1033
export class UpdateCategoryDto {
11-
@IsNotEmpty()
12-
name: string;
34+
@IsUUID()
35+
id: string;
36+
37+
@ValidateNested()
38+
@Type(() => CategoryTranslationsDto)
39+
translations: CategoryTranslationsDto;
40+
41+
@IsOptional()
42+
@IsUUID()
43+
parent_id?: string | null;
1344
}

backend/src/modules/categories/dto/params.dto.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import { Type } from "class-transformer";
1+
import { Transform, Type } from "class-transformer";
22
import {
3+
IsBoolean,
34
IsNotEmpty,
45
IsNumber,
56
IsOptional,
7+
IsString,
68
IsUUID,
79
ValidateNested,
810
} from "class-validator";
@@ -13,12 +15,25 @@ export class GetParamsDto {
1315
@Type(() => Number)
1416
@IsNumber()
1517
@IsOptional()
16-
page: number = 1;
18+
page: number;
1719

1820
@Type(() => Number)
1921
@IsNumber()
2022
@IsOptional()
21-
limit: number = 10;
23+
limit: number;
24+
25+
@IsOptional()
26+
@IsString()
27+
search: string;
28+
29+
@IsOptional()
30+
@IsString()
31+
order: string;
32+
33+
@IsOptional()
34+
@IsBoolean()
35+
@Transform(({ value }) => value === "asc")
36+
asc: boolean;
2237
}
2338

2439
export class CreateParamsDto {

backend/src/modules/storage-items/storage-items.service.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,20 @@ export class StorageItemsService {
143143
org_filter?: string,
144144
) {
145145
// Build a base query without range for counting and apply all filters
146+
147+
// Get nested categories of X category ID.
148+
const matchingCategories: string[] = [];
149+
if (category) {
150+
const { data: categories } = await supabase.rpc(
151+
"get_category_descendants",
152+
{
153+
category_uuid: category,
154+
},
155+
);
156+
matchingCategories.push(
157+
...(categories as { id: string }[]).map((c) => c.id),
158+
);
159+
}
146160
const base = applyItemFilters(
147161
supabase
148162
.from("view_manage_storage_items")
@@ -153,7 +167,7 @@ export class StorageItemsService {
153167
isActive,
154168
tags,
155169
location_filter,
156-
category,
170+
categories: matchingCategories,
157171
from_date,
158172
to_date,
159173
availability_min,
@@ -193,7 +207,7 @@ export class StorageItemsService {
193207
isActive,
194208
tags,
195209
location_filter,
196-
category,
210+
categories: matchingCategories,
197211
from_date,
198212
to_date,
199213
availability_min,
@@ -202,7 +216,6 @@ export class StorageItemsService {
202216
});
203217

204218
if (order_by) query.order(order_by ?? "created_at", { ascending });
205-
206219
const result = await query;
207220

208221
if (result.error) {
@@ -250,7 +263,19 @@ export class StorageItemsService {
250263
if (!supabase) {
251264
throw new BadRequestException("Supabase client is not initialized.");
252265
}
253-
266+
// Get nested categories of X category ID.
267+
const matchingCategories: string[] = [];
268+
if (category) {
269+
const { data: categories } = await supabase.rpc(
270+
"get_category_descendants",
271+
{
272+
category_uuid: category,
273+
},
274+
);
275+
matchingCategories.push(
276+
...(categories as { id: string }[]).map((c) => c.id),
277+
);
278+
}
254279
// Build a base query with organization filtering
255280
const base = applyItemFilters(
256281
supabase
@@ -263,7 +288,7 @@ export class StorageItemsService {
263288
isActive,
264289
tags,
265290
location_filter,
266-
category,
291+
categories: matchingCategories,
267292
},
268293
);
269294
const countResult = await base;
@@ -299,7 +324,7 @@ export class StorageItemsService {
299324
isActive,
300325
tags,
301326
location_filter,
302-
category,
327+
categories: matchingCategories,
303328
});
304329

305330
if (order_by) query.order(order_by ?? "created_at", { ascending });
@@ -324,10 +349,6 @@ export class StorageItemsService {
324349
* @returns An object containing the total count of storage items.
325350
*/
326351
async getItemCount(req: AuthRequest, role: string, orgId: string) {
327-
console.log("Request path:", req.path);
328-
console.log("Headers:", req.headers);
329-
console.log("ActiveRoleContext:", req.activeRoleContext);
330-
331352
const supabase = req.supabase;
332353
const result = await supabase
333354
.from("storage_items")

backend/src/utils/pagination.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export function getPaginationRange(
66
// ensure valid numbers - safer fallback
77
const safePage = Number.isFinite(page) && page > 0 ? page : 1; // at least 1
88
const safeLimit = Number.isFinite(limit) && limit > 0 ? limit : 10; // at least 10
9+
910
// calculations
1011
const from = (safePage - 1) * safeLimit;
1112
const to = from + safeLimit - 1;

backend/src/utils/storage-items.utils.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ export function applyItemFilters<T extends FilterableQuery>(
164164
isActive?: boolean;
165165
tags?: string;
166166
location_filter?: string;
167-
category?: string;
167+
categories?: string[];
168168
from_date?: string;
169169
to_date?: string;
170170
availability_min?: number;
@@ -177,7 +177,7 @@ export function applyItemFilters<T extends FilterableQuery>(
177177
isActive,
178178
tags,
179179
location_filter,
180-
category,
180+
categories,
181181
from_date,
182182
to_date,
183183
availability_min,
@@ -197,7 +197,6 @@ export function applyItemFilters<T extends FilterableQuery>(
197197
}
198198

199199
if (typeof isActive === "boolean") query.eq("is_active", isActive);
200-
201200
if (tags) query.overlaps("tag_ids", tags.split(","));
202201

203202
if (location_filter) {
@@ -210,7 +209,7 @@ export function applyItemFilters<T extends FilterableQuery>(
210209

211210
if (org_filter) query.in("organization_id", org_filter.split(","));
212211

213-
if (category) query.in("en_item_type", category.split(","));
212+
if (categories && categories.length > 0) query.in("category_id", categories);
214213

215214
if (from_date) query.gte("created_at", from_date);
216215
if (to_date) query.lt("created_at", to_date);

common/items/categories.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Database } from "@common/database.types";
2+
import { StripNullFrom } from "@common/helper.types";
3+
4+
export type CategoryViewRow =
5+
Database["public"]["Views"]["view_category_details"]["Row"];
6+
export type CategoryTranslations = {
7+
translations: {
8+
en: string;
9+
fi: string;
10+
};
11+
};
12+
13+
export type Category = Omit<StripNullFrom<CategoryViewRow, "id" | "assigned_to" | "created_at">, "translations"> &
14+
CategoryTranslations;
15+
export type CategoryInsert =
16+
Database["public"]["Tables"]["categories"]["Insert"];
17+
export type CategoryUpdate =
18+
Database["public"]["Tables"]["categories"]["Update"];

0 commit comments

Comments
 (0)