diff --git a/frontend/app/(auth)/forgot-password/page.tsx b/frontend/app/(auth)/forgot-password/page.tsx new file mode 100644 index 000000000..846bdfc31 --- /dev/null +++ b/frontend/app/(auth)/forgot-password/page.tsx @@ -0,0 +1,121 @@ +'use client'; + +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import Link from 'next/link'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; + +const schema = z.object({ + email: z.string().email('Enter a valid email address'), +}); + +type FormValues = z.infer; + +export default function ForgotPasswordPage() { + const [submitted, setSubmitted] = useState(false); + const [networkError, setNetworkError] = useState(null); + + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm({ resolver: zodResolver(schema) }); + + const onSubmit = async (values: FormValues) => { + setNetworkError(null); + try { + const res = await fetch('/api/auth/forgot-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: values.email }), + }); + + if (!res.ok && res.status >= 500) { + // Treat 5xx as a network-level error; 4xx are intentionally swallowed + // per the security best-practice of always returning 200. + throw new Error('Server error. Please try again later.'); + } + + // Always show the success message regardless of whether the email exists. + setSubmitted(true); + } catch (err: unknown) { + setNetworkError( + (err as Error)?.message ?? 'Something went wrong. Please try again.', + ); + } + }; + + if (submitted) { + return ( + <> +
+
+ + + +
+

Check your inbox

+

+ If an account with that email exists, a reset link has been sent. Check your inbox. +

+
+ + ← Back to Login + + + ); + } + + return ( + <> +

Forgot your password?

+

+ Enter your email and we'll send you a reset link. +

+ +
+ + + {networkError && ( +

+ {networkError} +

+ )} + + +
+ +

+ + ← Back to Login + +

+ + ); +} \ No newline at end of file diff --git a/frontend/app/(dashboard)/assets/[id]/page.tsx b/frontend/app/(dashboard)/assets/[id]/page.tsx index 3333bbbf2..f48ec3c45 100644 --- a/frontend/app/(dashboard)/assets/[id]/page.tsx +++ b/frontend/app/(dashboard)/assets/[id]/page.tsx @@ -19,7 +19,6 @@ import { Upload, Plus, Printer, - QrCode, } from "lucide-react"; import { format } from "date-fns"; import { Button } from "@/components/ui/button"; @@ -39,8 +38,16 @@ import { useUpdateMaintenanceStatus, useCreateNote, useDeleteNote, + useUpdateAssetStatus, } from "@/lib/query/hooks/useAsset"; -import type { MaintenanceType } from "@/lib/query/types/asset"; +import type { AssetStatus, MaintenanceType } from "@/lib/query/types/asset"; + +const STATUS_TRANSITIONS: Record = { + ACTIVE: ["INACTIVE", "MAINTENANCE", "RETIRED"], + INACTIVE: ["ACTIVE", "RETIRED"], + MAINTENANCE: ["ACTIVE", "INACTIVE", "RETIRED"], + RETIRED: [], // terminal — no transitions +}; type Tab = "overview" | "history" | "maintenance" | "documents" | "notes"; @@ -250,12 +257,90 @@ function UploadDocumentModal({ ); } +// ── ChangeStatusModal ──────────────────────────────────────────────────────── +function ChangeStatusModal({ + assetId, + currentStatus, + onClose, +}: { + assetId: string; + currentStatus: string; + onClose: () => void; +}) { + const allowedTransitions = STATUS_TRANSITIONS[currentStatus] ?? []; + const [selectedStatus, setSelectedStatus] = useState(allowedTransitions[0] ?? ""); + const [apiError, setApiError] = useState(null); + + const { mutate: updateStatus, isPending } = useUpdateAssetStatus(assetId, { + onSuccess: (updated) => { + // toast is handled in the parent via onSuccess callback + onClose(); + }, + onError: (err) => { + setApiError(err?.message ?? "Failed to update status. Please try again."); + }, + }); + + return ( +
+
+
+

Change Asset Status

+

+ Current status:{" "} + {currentStatus} +

+ +
+ + +
+ + {apiError && ( +

+ {apiError} +

+ )} + +
+ + +
+
+
+ ); +} + // ── Main Page ──────────────────────────────────────────────────────────────── export default function AssetDetailPage() { const { id } = useParams<{ id: string }>(); const router = useRouter(); const [tab, setTab] = useState("overview"); const [qrCodeDataUri, setQrCodeDataUri] = useState(null); + const [showChangeStatus, setShowChangeStatus] = useState(false); +const [statusToastMsg, setStatusToastMsg] = useState(null); // Confirm dialogs const [confirmDelete, setConfirmDelete] = useState(false); @@ -387,16 +472,21 @@ export default function AssetDetailPage() { - +