fix: allow admins only to block with details in parental control page (mobile view) 🐛
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 9m39s

This commit is contained in:
2025-09-20 19:07:08 +05:00
parent 035cd02012
commit 5277c13fb7
3 changed files with 234 additions and 232 deletions

View File

@@ -6,12 +6,12 @@ import { useActionState, useEffect, useState, useTransition } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import type { Device } from "@/lib/backend-types"; import type { Device } from "@/lib/backend-types";
@@ -21,151 +21,151 @@ import { TextShimmer } from "./ui/text-shimmer";
import { Textarea } from "./ui/textarea"; import { Textarea } from "./ui/textarea";
export type BlockDeviceFormState = { export type BlockDeviceFormState = {
message: string; message: string;
success: boolean; success: boolean;
fieldErrors?: { fieldErrors?: {
reason_for_blocking?: string[]; reason_for_blocking?: string[];
}; };
payload?: FormData; payload?: FormData;
}; };
const initialState: BlockDeviceFormState = { const initialState: BlockDeviceFormState = {
message: "", message: "",
success: false, success: false,
fieldErrors: {}, fieldErrors: {},
}; };
export default function BlockDeviceDialog({ export default function BlockDeviceDialog({
device, device,
// admin, admin,
parentalControl = false, parentalControl = false,
}: { }: {
device: Device; device: Device;
type: "block" | "unblock"; type: "block" | "unblock";
admin?: boolean; admin?: boolean;
parentalControl?: boolean; parentalControl?: boolean;
}) { }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [state, formAction, isPending] = useActionState( const [state, formAction, isPending] = useActionState(
blockDeviceAction, blockDeviceAction,
initialState, initialState,
); );
const [isTransitioning, startTransition] = useTransition(); const [isTransitioning, startTransition] = useTransition();
const handleSimpleBlock = () => { const handleSimpleBlock = () => {
startTransition(() => { startTransition(() => {
const formData = new FormData(); const formData = new FormData();
formData.append("deviceId", String(device.id)); formData.append("deviceId", String(device.id));
formData.append("reason_for_blocking", ""); formData.append("reason_for_blocking", "");
formData.append("action", "simple-block"); formData.append("action", "simple-block");
formData.append("blocked_by", "PARENT"); formData.append("blocked_by", "PARENT");
formAction(formData); formAction(formData);
}); });
}; };
const handleUnblock = () => { const handleUnblock = () => {
startTransition(() => { startTransition(() => {
const formData = new FormData(); const formData = new FormData();
formData.append("deviceId", String(device.id)); formData.append("deviceId", String(device.id));
formData.append("reason_for_blocking", ""); formData.append("reason_for_blocking", "");
formData.append("action", "unblock"); formData.append("action", "unblock");
formData.append("blocked_by", "PARENT"); formData.append("blocked_by", "PARENT");
formAction(formData); formAction(formData);
}); });
}; };
// Show toast notifications based on state changes // Show toast notifications based on state changes
useEffect(() => { useEffect(() => {
if (state.message) { if (state.message) {
if (state.success) { if (state.success) {
toast.success(state.message); toast.success(state.message);
if (open) setOpen(false); if (open) setOpen(false);
} else { } else {
toast.error(state.message); toast.error(state.message);
} }
} }
}, [state, open]); }, [state, open]);
const isLoading = isPending || isTransitioning; const isLoading = isPending || isTransitioning;
// If device is blocked and user is not admin, show unblock button // If device is blocked and user is not admin, show unblock button
if (device.blocked && parentalControl) { if (device.blocked && parentalControl) {
return ( return (
<Button onClick={handleUnblock} disabled={isLoading}> <Button onClick={handleUnblock} disabled={isLoading}>
{isLoading ? <TextShimmer>Unblocking</TextShimmer> : "Unblock"} {isLoading ? <TextShimmer>Unblocking</TextShimmer> : "Unblock"}
</Button> </Button>
); );
} }
// If device is not blocked and user is not admin, show simple block button // If device is not blocked and user is not admin, show simple block button
if (!device.blocked && parentalControl) { if ((!device.blocked && parentalControl) || !admin) {
return ( return (
<Button <Button
onClick={handleSimpleBlock} onClick={handleSimpleBlock}
disabled={isLoading} disabled={isLoading}
variant="destructive" variant="destructive"
> >
<ShieldBan /> <ShieldBan />
{isLoading ? <TextShimmer>Blocking</TextShimmer> : "Block"} {isLoading ? <TextShimmer>Blocking</TextShimmer> : "Block"}
</Button> </Button>
); );
} }
// If user is admin, show block with reason dialog // If user is admin, show block with reason dialog
return ( return (
<div> <div>
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button disabled={isLoading} variant="destructive"> <Button disabled={isLoading} variant="destructive">
<OctagonX /> <OctagonX />
Block Block
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-[425px]"> <DialogContent className="sm:max-w-[425px]">
<DialogHeader> <DialogHeader>
<DialogTitle>Block 🚫</DialogTitle> <DialogTitle>Block 🚫</DialogTitle>
<DialogDescription className="text-sm text-muted-foreground"> <DialogDescription className="text-sm text-muted-foreground">
Please provide a reason for blocking this device Please provide a reason for blocking this device
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<form action={formAction} className="space-y-4"> <form action={formAction} className="space-y-4">
<input type="hidden" name="deviceId" value={String(device.id)} /> <input type="hidden" name="deviceId" value={String(device.id)} />
<input type="hidden" name="action" value="block" /> <input type="hidden" name="action" value="block" />
<input type="hidden" name="blocked_by" value="ADMIN" /> <input type="hidden" name="blocked_by" value="ADMIN" />
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
<div className="flex flex-col items-start gap-1"> <div className="flex flex-col items-start gap-1">
<Label htmlFor="reason_for_blocking" className="text-right"> <Label htmlFor="reason_for_blocking" className="text-right">
Reason for blocking Reason for blocking
</Label> </Label>
<Textarea <Textarea
rows={10} rows={10}
name="reason_for_blocking" name="reason_for_blocking"
id="reason_for_blocking" id="reason_for_blocking"
defaultValue={ defaultValue={
(state?.payload?.get("reason_for_blocking") || "") as string (state?.payload?.get("reason_for_blocking") || "") as string
} }
className={cn( className={cn(
"col-span-5 mt-2", "col-span-5 mt-2",
state.fieldErrors?.reason_for_blocking && state.fieldErrors?.reason_for_blocking &&
"ring-2 ring-red-500", "ring-2 ring-red-500",
)} )}
/> />
<span className="text-sm text-red-500"> <span className="text-sm text-red-500">
{state.fieldErrors?.reason_for_blocking?.[0]} {state.fieldErrors?.reason_for_blocking?.[0]}
</span> </span>
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<Button variant="destructive" disabled={isLoading} type="submit"> <Button variant="destructive" disabled={isLoading} type="submit">
{isLoading ? "Blocking..." : "Block"} {isLoading ? "Blocking..." : "Block"}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form> </form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</div> </div>
); );
} }

View File

@@ -10,107 +10,108 @@ import BlockDeviceDialog from "./block-device-dialog";
import { Badge } from "./ui/badge"; import { Badge } from "./ui/badge";
export default function DeviceCard({ export default function DeviceCard({
device, device,
parentalControl, parentalControl,
isAdmin,
}: { }: {
device: Device; device: Device;
parentalControl?: boolean; parentalControl?: boolean;
isAdmin?: boolean; isAdmin?: boolean;
}) { }) {
const [devices, setDeviceCart] = useAtom(deviceCartAtom); const [devices, setDeviceCart] = useAtom(deviceCartAtom);
const isChecked = devices.some((d) => d.id === device.id); const isChecked = devices.some((d) => d.id === device.id);
return ( return (
// biome-ignore lint/a11y/noStaticElementInteractions: <dw about it> // biome-ignore lint/a11y/noStaticElementInteractions: <dw about it>
<div <div
onKeyUp={() => {}} onKeyUp={() => {}}
onClick={() => { onClick={() => {
if (device.blocked) return; if (device.blocked) return;
if (device.is_active === true) return; if (device.is_active === true) return;
if (device.has_a_pending_payment === true) return; if (device.has_a_pending_payment === true) return;
if (parentalControl === true) return; if (parentalControl === true) return;
setDeviceCart((prev) => setDeviceCart((prev) =>
devices.some((d) => d.id === device.id) devices.some((d) => d.id === device.id)
? prev.filter((d) => d.id !== device.id) ? prev.filter((d) => d.id !== device.id)
: [...prev, device], : [...prev, device],
); );
}} }}
className="w-full" className="w-full"
> >
<div <div
className={cn( className={cn(
"flex text-sm justify-between items-center my-2 p-4 border rounded-md", "flex text-sm justify-between items-center my-2 p-4 border rounded-md",
isChecked ? "bg-accent" : "", isChecked ? "bg-accent" : "",
device.is_active device.is_active
? "cursor-not-allowed text-green-600 hover:bg-accent-foreground/10" ? "cursor-not-allowed text-green-600 hover:bg-accent-foreground/10"
: "cursor-pointer hover:bg-muted-foreground/10", : "cursor-pointer hover:bg-muted-foreground/10",
)} )}
> >
<div className=""> <div className="">
<div className="font-semibold flex flex-col items-start gap-2 mb-2"> <div className="font-semibold flex flex-col items-start gap-2 mb-2">
<Link <Link
className={cn( className={cn(
"font-medium hover:underline ml-0.5", "font-medium hover:underline ml-0.5",
device.is_active ? "text-green-600" : "", device.is_active ? "text-green-600" : "",
)} )}
href={`/devices/${device.id}`} href={`/devices/${device.id}`}
> >
{device.name} {device.name}
</Link> </Link>
<Badge variant={"outline"}> <Badge variant={"outline"}>
<span className="font-medium">{device.mac}</span> <span className="font-medium">{device.mac}</span>
</Badge> </Badge>
<Badge variant={"outline"}> <Badge variant={"outline"}>
<span className="font-medium">{device.vendor}</span> <span className="font-medium">{device.vendor}</span>
</Badge> </Badge>
</div> </div>
{device.is_active ? ( {device.is_active ? (
<div className="text-muted-foreground ml-0.5"> <div className="text-muted-foreground ml-0.5">
Active until{" "} Active until{" "}
<span className="font-semibold"> <span className="font-semibold">
{new Date(device.expiry_date || "").toLocaleDateString( {new Date(device.expiry_date || "").toLocaleDateString(
"en-US", "en-US",
{ {
month: "short", month: "short",
day: "2-digit", day: "2-digit",
year: "numeric", year: "numeric",
}, },
)} )}
</span> </span>
</div> </div>
) : ( ) : (
<p className="text-muted-foreground ml-0.5">Device Inactive</p> <p className="text-muted-foreground ml-0.5">Device Inactive</p>
)} )}
{device.has_a_pending_payment && ( {device.has_a_pending_payment && (
<Link href={`/payments/${device.pending_payment_id}`}> <Link href={`/payments/${device.pending_payment_id}`}>
<span className="bg-muted rounded px-2 p-1 mt-2 flex hover:underline items-center justify-center gap-2 text-yellow-600"> <span className="bg-muted rounded px-2 p-1 mt-2 flex hover:underline items-center justify-center gap-2 text-yellow-600">
Payment Pending{" "} Payment Pending{" "}
<HandCoins className="animate-pulse" size={14} /> <HandCoins className="animate-pulse" size={14} />
</span> </span>
</Link> </Link>
)} )}
{device.blocked && device.blocked_by === "ADMIN" && ( {device.blocked && device.blocked_by === "ADMIN" && (
<div className="p-2 rounded border my-2 w-full"> <div className="p-2 rounded border my-2 w-full">
<span className="uppercase text-red-500">Blocked by admin </span> <span className="uppercase text-red-500">Blocked by admin </span>
<p className="text-neutral-500">{device?.reason_for_blocking}</p> <p className="text-neutral-500">{device?.reason_for_blocking}</p>
</div> </div>
)} )}
</div> </div>
<div> <div>
{!parentalControl ? ( {!parentalControl ? (
<AddDevicesToCartButton device={device} /> <AddDevicesToCartButton device={device} />
) : ( ) : (
<BlockDeviceDialog <BlockDeviceDialog
admin={false} admin={isAdmin}
type={device.blocked ? "unblock" : "block"} type={device.blocked ? "unblock" : "block"}
device={device} device={device}
/> />
)} )}
</div> </div>
</div> </div>
</div> </div>
); );
} }

View File

@@ -109,6 +109,7 @@ export async function DevicesTable({
parentalControl={parentalControl} parentalControl={parentalControl}
key={device.id} key={device.id}
device={device} device={device}
isAdmin={isAdmin}
/> />
))} ))}
</div> </div>