Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions frontend/app/(auth)/forgot-password/page.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof schema>;

export default function ForgotPasswordPage() {
const [submitted, setSubmitted] = useState(false);
const [networkError, setNetworkError] = useState<string | null>(null);

const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormValues>({ 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 (
<>
<div className="flex flex-col items-center text-center mb-6">
<div className="w-12 h-12 rounded-full bg-green-100 flex items-center justify-center mb-4">
<svg
className="w-6 h-6 text-green-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<h2 className="text-xl font-semibold text-gray-900 mb-1">Check your inbox</h2>
<p className="text-sm text-gray-500">
If an account with that email exists, a reset link has been sent. Check your inbox.
</p>
</div>
<Link
href="/login"
className="block text-center text-sm font-medium text-gray-900 hover:underline"
>
← Back to Login
</Link>
</>
);
}

return (
<>
<h2 className="text-xl font-semibold text-gray-900 mb-1">Forgot your password?</h2>
<p className="text-sm text-gray-500 mb-6">
Enter your email and we'll send you a reset link.
</p>

<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<Input
id="email"
label="Email address"
type="email"
placeholder="you@company.com"
autoComplete="email"
{...register('email')}
error={errors.email?.message}
/>

{networkError && (
<p className="text-sm text-red-500 bg-red-50 border border-red-200 rounded-lg px-3 py-2">
{networkError}
</p>
)}

<Button type="submit" size="lg" loading={isSubmitting} className="w-full mt-2">
Send reset link
</Button>
</form>

<p className="text-center text-sm text-gray-500 mt-6">
<Link href="/login" className="font-medium text-gray-900 hover:underline">
← Back to Login
</Link>
</p>
</>
);
}
114 changes: 105 additions & 9 deletions frontend/app/(dashboard)/assets/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
Upload,
Plus,
Printer,
QrCode,
} from "lucide-react";
import { format } from "date-fns";
import { Button } from "@/components/ui/button";
Expand All @@ -39,8 +38,16 @@
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<string, string[]> = {
ACTIVE: ["INACTIVE", "MAINTENANCE", "RETIRED"],
INACTIVE: ["ACTIVE", "RETIRED"],
MAINTENANCE: ["ACTIVE", "INACTIVE", "RETIRED"],
RETIRED: [], // terminal — no transitions
};

type Tab = "overview" | "history" | "maintenance" | "documents" | "notes";

Expand Down Expand Up @@ -250,12 +257,90 @@
);
}

// ── 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<string | null>(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 (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/40" onClick={onClose} />
<div className="relative bg-white rounded-2xl shadow-xl w-full max-w-sm p-6">
<h3 className="text-base font-semibold text-gray-900 mb-1">Change Asset Status</h3>
<p className="text-sm text-gray-500 mb-4">
Current status:{" "}
<span className="font-medium text-gray-700">{currentStatus}</span>
</p>

<div className="mb-4">
<label className="block text-xs font-medium text-gray-600 mb-1">New Status</label>
<select
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gray-900/10"
value={selectedStatus}
onChange={(e) => {
setSelectedStatus(e.target.value);
setApiError(null);
}}
>
{allowedTransitions.map((s) => (
<option key={s} value={s}>
{s.charAt(0) + s.slice(1).toLowerCase()}
</option>
))}
</select>
</div>

{apiError && (
<p className="text-sm text-red-500 bg-red-50 border border-red-200 rounded-lg px-3 py-2 mb-4">
{apiError}
</p>
)}

<div className="flex gap-3">
<Button variant="outline" className="flex-1" onClick={onClose} disabled={isPending}>
Cancel
</Button>
<Button
className="flex-1"
loading={isPending}
disabled={!selectedStatus}
onClick={() => updateStatus({ status: selectedStatus as AssetStatus })}
>
Confirm
</Button>
</div>
</div>
</div>
);
}

// ── Main Page ────────────────────────────────────────────────────────────────
export default function AssetDetailPage() {
const { id } = useParams<{ id: string }>();
const router = useRouter();
const [tab, setTab] = useState<Tab>("overview");
const [qrCodeDataUri, setQrCodeDataUri] = useState<string | null>(null);
const [showChangeStatus, setShowChangeStatus] = useState(false);
const [statusToastMsg, setStatusToastMsg] = useState<string | null>(null);

// Confirm dialogs
const [confirmDelete, setConfirmDelete] = useState(false);
Expand Down Expand Up @@ -387,16 +472,21 @@
<Button size="sm" variant="outline">
<ArrowRightLeft size={14} className="mr-1.5" /> Transfer
</Button>
<Button size="sm" variant="outline">
<RefreshCw size={14} className="mr-1.5" /> Update Status
</Button>
<Button
size="sm"
variant="outline"
disabled={STATUS_TRANSITIONS[asset.status]?.length === 0}
onClick={() => setShowChangeStatus(true)}
>
<RefreshCw size={14} className="mr-1.5" /> Update Status
</Button>
<Button size="sm" variant="outline">
<Pencil size={14} className="mr-1.5" /> Edit
</Button>
<Button
size="sm"
variant="outline"
className="!text-red-600 !border-red-200 hover:!bg-red-50"
className="text-red-600 border-red-200 hover:bg-red-50"
onClick={() => setConfirmDelete(true)}
>
<Trash2 size={14} className="mr-1.5" /> Delete
Expand Down Expand Up @@ -502,7 +592,7 @@
<div className="bg-white rounded-xl border border-gray-200 p-5 print:border-0 print:p-0">
<h2 className="text-sm font-semibold text-gray-900 mb-4 print:hidden">QR Code</h2>
<div className="flex justify-center print:justify-start">
<img

Check warning on line 595 in frontend/app/(dashboard)/assets/[id]/page.tsx

View workflow job for this annotation

GitHub Actions / Frontend (Next.js)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element

Check warning on line 595 in frontend/app/(dashboard)/assets/[id]/page.tsx

View workflow job for this annotation

GitHub Actions / Frontend (Next.js)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
src={qrCodeDataUri}
alt={`QR Code for ${asset.name}`}
className="w-32 h-32 print:w-40 print:h-40"
Expand Down Expand Up @@ -655,7 +745,7 @@
<Button
size="sm"
variant="ghost"
className="!text-red-500"
className="text-red-500"
onClick={() => setConfirmDeleteDoc(doc.id)}
>
<Trash2 size={14} />
Expand Down Expand Up @@ -713,7 +803,7 @@
<Button
size="sm"
variant="ghost"
className="!text-red-500 shrink-0"
className="text-red-500 shrink-0"
onClick={() => setConfirmDeleteNote(note.id)}
>
<Trash2 size={14} />
Expand Down Expand Up @@ -778,7 +868,13 @@
onClose={() => setShowScheduleMaintenance(false)}
/>
)}

{showChangeStatus && (
<ChangeStatusModal
assetId={id}
currentStatus={asset.status}
onClose={() => setShowChangeStatus(false)}
/>
)}
{showUploadDoc && (
<UploadDocumentModal assetId={id} onClose={() => setShowUploadDoc(false)} />
)}
Expand Down
Loading