Skip to content

Commit b5f4402

Browse files
feat: add room task component (#264)
* feat: add task cards to rooms * feat: optional assign to self field * feat: no tag for low prio + rem sizing * fix: rename tasks to request
1 parent 996bf5f commit b5f4402

File tree

6 files changed

+203
-26
lines changed

6 files changed

+203
-26
lines changed

clients/web/src/components/rooms/OverviewCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function OverviewCard({
5555
col.valueSecondary != null && "whitespace-nowrap",
5656
)}
5757
>
58-
<span className="text-[32px] font-bold text-text-default">
58+
<span className="text-2xl lg:text-[32px] font-bold text-text-default">
5959
{col.value}
6060
</span>
6161
{col.valueSecondary != null && (
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { FlagIcon, MapPinIcon, Maximize2Icon, StoreIcon } from "lucide-react";
2+
import type { LucideIcon } from "lucide-react";
3+
4+
import { Button } from "@/components/ui/Button";
5+
import { cn } from "@/lib/utils";
6+
7+
type Priority = "high" | "medium" | "low";
8+
9+
type PriorityConfig = {
10+
label: string;
11+
Icon: LucideIcon;
12+
containerClass: string;
13+
contentClass: string;
14+
};
15+
16+
const priorityConfig: Record<Exclude<Priority, "low">, PriorityConfig> = {
17+
high: {
18+
label: "High Priority",
19+
Icon: FlagIcon,
20+
containerClass: "bg-bg-high-priority",
21+
contentClass: "text-high-priority",
22+
},
23+
medium: {
24+
label: "Medium Priority",
25+
Icon: FlagIcon,
26+
containerClass: "bg-bg-orange",
27+
contentClass: "text-text-orange",
28+
},
29+
};
30+
31+
export type RoomRequestCardData = {
32+
title: string;
33+
floor: number;
34+
roomNumber: number;
35+
department: string;
36+
priority?: Priority;
37+
assignedTo?: string | null;
38+
};
39+
40+
export type RoomRequestCardProps = RoomRequestCardData & {
41+
onAssignToSelf?: () => void;
42+
onExpand?: () => void;
43+
className?: string;
44+
};
45+
46+
export function RoomRequestCard({
47+
title,
48+
floor,
49+
roomNumber,
50+
department,
51+
priority,
52+
assignedTo,
53+
onAssignToSelf,
54+
onExpand,
55+
className = "",
56+
}: RoomRequestCardProps) {
57+
return (
58+
<div
59+
className={cn(
60+
"flex flex-col gap-3 rounded border border-stroke-disabled bg-bg-primary px-3 py-4",
61+
className,
62+
)}
63+
>
64+
<div className="flex flex-col gap-2">
65+
<div className="flex items-start justify-between gap-2">
66+
<span className="text-base font-medium leading-snug tracking-tight text-text-default">
67+
{title}
68+
</span>
69+
<button
70+
type="button"
71+
onClick={onExpand}
72+
className="shrink-0 text-text-subtle hover:text-text-default"
73+
aria-label="Expand request"
74+
>
75+
<Maximize2Icon className="size-[1.125rem]" strokeWidth={1.5} />
76+
</button>
77+
</div>
78+
79+
<div className="flex items-center gap-1">
80+
<MapPinIcon
81+
className="size-3 shrink-0 text-text-subtle"
82+
strokeWidth={1.5}
83+
/>
84+
<span className="text-xs text-text-subtle">
85+
Floor {floor}, Room {roomNumber}
86+
</span>
87+
</div>
88+
89+
<div className="flex flex-wrap items-center gap-3">
90+
{priority &&
91+
(priority == "medium" || priority == "high") &&
92+
(() => {
93+
const { label, Icon, containerClass, contentClass } =
94+
priorityConfig[priority];
95+
return (
96+
<div
97+
className={cn(
98+
"inline-flex items-center gap-1 rounded px-2 py-1",
99+
containerClass,
100+
)}
101+
>
102+
<Icon
103+
className={cn("size-4", contentClass)}
104+
strokeWidth={2}
105+
/>
106+
<span className={cn("text-xs", contentClass)}>{label}</span>
107+
</div>
108+
);
109+
})()}
110+
<div className="inline-flex items-center gap-2 rounded border border-stroke-subtle bg-bg-primary px-2 py-1">
111+
<StoreIcon
112+
className="size-3 shrink-0 text-text-default"
113+
strokeWidth={1.5}
114+
/>
115+
<span className="text-xs text-text-default">{department}</span>
116+
</div>
117+
</div>
118+
</div>
119+
120+
{!assignedTo && (
121+
<Button variant="primary" className="w-full" onClick={onAssignToSelf}>
122+
Assign to Self
123+
</Button>
124+
)}
125+
</div>
126+
);
127+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { RoomRequestCardData } from "@/components/rooms/RoomRequestCard";
2+
import { cn } from "@/lib/utils";
3+
import { RoomRequestCard } from "@/components/rooms/RoomRequestCard";
4+
5+
export type RoomRequestItem = RoomRequestCardData & { id: string };
6+
7+
type RoomRequestListProps = {
8+
title: string;
9+
requests: Array<RoomRequestItem>;
10+
onAssignToSelf?: (id: string) => void;
11+
onExpand?: (id: string) => void;
12+
className?: string;
13+
};
14+
15+
export function RoomRequestList({
16+
title,
17+
requests,
18+
onAssignToSelf,
19+
onExpand,
20+
className = "",
21+
}: RoomRequestListProps) {
22+
return (
23+
<section className={cn("flex w-full min-w-0 flex-col", className)}>
24+
<h2 className="my-2 shrink-0 text-sm font-medium leading-tight text-neutral-400">
25+
{title} ({requests.length})
26+
</h2>
27+
28+
<div className="h-0.5 w-full shrink-0 bg-stroke-subtle" />
29+
30+
<div className="mt-3 flex flex-col gap-2">
31+
{requests.map(({ id, ...request }) => (
32+
<RoomRequestCard
33+
key={id}
34+
{...request}
35+
onAssignToSelf={
36+
onAssignToSelf ? () => onAssignToSelf(id) : undefined
37+
}
38+
onExpand={onExpand ? () => onExpand(id) : undefined}
39+
/>
40+
))}
41+
</div>
42+
</section>
43+
);
44+
}

clients/web/src/components/rooms/RoomsOverview.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { RoomWithOptionalGuestBooking } from "@shared";
22
import { OverviewCard } from "@/components/rooms/OverviewCard";
3+
import { RoomRequestList } from "@/components/rooms/RoomRequestList";
34

45
type RoomsOverviewProps = {
56
rooms: Array<RoomWithOptionalGuestBooking>;
@@ -23,7 +24,7 @@ export function RoomsOverview({ rooms }: RoomsOverviewProps) {
2324
const vacantRooms = totalRooms - occupiedRooms;
2425

2526
return (
26-
<aside className="w-1/4 shrink-0 min-h-0 overflow-y-auto px-6">
27+
<aside className="w-full max-w-[24.875rem] shrink-0 min-h-0 overflow-y-auto px-6">
2728
<div className="flex flex-col">
2829
<OverviewCard
2930
title="Tasks"
@@ -39,7 +40,11 @@ export function RoomsOverview({ rooms }: RoomsOverviewProps) {
3940
value: cleaningOnlyRooms,
4041
description: "Tasks",
4142
},
42-
{ field: "Pending", value: cleaningRooms, description: "Tasks" },
43+
{
44+
field: "Pending",
45+
value: cleaningRooms,
46+
description: "Tasks",
47+
},
4348
]}
4449
/>
4550

@@ -64,6 +69,28 @@ export function RoomsOverview({ rooms }: RoomsOverviewProps) {
6469
},
6570
]}
6671
/>
72+
<RoomRequestList
73+
title="Unassigned Tasks"
74+
requests={[
75+
{
76+
id: "1",
77+
title: "Room 101",
78+
floor: 1,
79+
roomNumber: 101,
80+
department: "Maintenance",
81+
priority: "low",
82+
},
83+
{
84+
id: "2",
85+
title: "Room 102",
86+
floor: 1,
87+
roomNumber: 102,
88+
department: "Maintenance",
89+
priority: "medium",
90+
assignedTo: "John Doe",
91+
},
92+
]}
93+
/>
6794
</div>
6895
</aside>
6996
);

clients/web/src/components/rooms/TaskCard.tsx

Lines changed: 0 additions & 23 deletions
This file was deleted.

clients/web/src/styles.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,11 @@
5050
--color-bg-container: #f8f8f8;
5151
--color-bg-disabled: #bababa;
5252
--color-bg-primary: #ffffff;
53+
--color-bg-orange: #fff3ed;
5354
--color-bg-secondary: #5d5d5d;
5455
--color-bg-high-priority: #ffeded;
5556
--color-text-default: #000;
57+
--color-text-orange: #ff8c3f;
5658
--color-text-subtle: #747474;
5759
--color-text-secondary: #5d5d5d;
5860
--color-bg-selected: #edf5f1;

0 commit comments

Comments
 (0)