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
22 changes: 18 additions & 4 deletions frontend/app/(dashboard)/assets/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import { ConditionBadge } from "@/components/assets/condition-badge";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { EditAssetModal } from "@/components/assets/edit-asset-modal";
import { InlineEdit } from "@/components/assets/inline-edit";
import { PhotoGallery } from "@/components/assets/photo-gallery";
import {
useAsset,
Expand Down Expand Up @@ -383,7 +384,12 @@
)}
<div className="flex items-center gap-2 mt-3">
<StatusBadge status={asset.status} />
<ConditionBadge condition={asset.condition} />
<InlineEdit
value={asset.condition}
label="Condition"
onSave={async (val) => { /* TODO: call API */ }}
display={<ConditionBadge condition={asset.condition} />}
/>
</div>
</div>
<div className="flex items-center gap-2 flex-wrap print:hidden">
Expand Down Expand Up @@ -468,9 +474,17 @@
<DetailRow
label="Current Value"
value={
asset.currentValue != null
? `$${Number(asset.currentValue).toLocaleString()}`
: undefined
<InlineEdit
value={asset.currentValue}
label="Current Value"
type="number"
onSave={async (val) => { /* TODO: call API */ }}
display={
asset.currentValue != null
? `$${Number(asset.currentValue).toLocaleString()}`
: "—"
}
/>
}
/>
<DetailRow
Expand Down Expand Up @@ -515,7 +529,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 532 in frontend/app/(dashboard)/assets/[id]/page.tsx

View workflow job for this annotation

GitHub Actions / Frontend Lint, Type-Check & Build

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 532 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
140 changes: 140 additions & 0 deletions frontend/app/(dashboard)/reservations/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"use client";

import { useState } from "react";
import { Calendar, Plus, Check, X, Clock } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";

interface Reservation {
id: string;
assetName: string;
assetId: string;
purpose: string;
startsAt: string;
endsAt: string;
status: "PENDING" | "CONFIRMED" | "CANCELLED" | "COMPLETED";
requester: string;
}

const MOCK: Reservation[] = [
{ id: "r1", assetName: "Projector A", assetId: "a-1", purpose: "Client demo", startsAt: "2026-08-26T09:00:00Z", endsAt: "2026-08-26T17:00:00Z", status: "PENDING", requester: "Alex" },
{ id: "r2", assetName: "Test Vehicle", assetId: "a-2", purpose: "Field audit", startsAt: "2026-08-27T08:00:00Z", endsAt: "2026-08-27T18:00:00Z", status: "CONFIRMED", requester: "Sam" },
];

const STATUS_COLORS: Record<string, string> = {
PENDING: "bg-yellow-100 text-yellow-700",
CONFIRMED: "bg-green-100 text-green-700",
CANCELLED: "bg-gray-100 text-gray-500",
COMPLETED: "bg-blue-100 text-blue-700",
};

type Tab = "my" | "pending";

export default function ReservationsPage() {
const [tab, setTab] = useState<Tab>("my");
const [reservations, setReservations] = useState<Reservation[]>(MOCK);
const [showBooking, setShowBooking] = useState(false);

const myReservations = reservations;
const pendingConfirmations = reservations.filter((r) => r.status === "PENDING");
const currentList = tab === "my" ? myReservations : pendingConfirmations;

const handleConfirm = (id: string) => {
setReservations((prev) => prev.map((r) => r.id === id ? { ...r, status: "CONFIRMED" as const } : r));
};

const handleCancel = (id: string) => {
setReservations((prev) => prev.map((r) => r.id === id ? { ...r, status: "CANCELLED" as const } : r));
};

return (
<div>
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold text-gray-900">Reservations</h1>
<p className="text-sm text-gray-500 mt-1">Book shared equipment and manage reservations</p>
</div>
<Button onClick={() => setShowBooking(true)}>
<Plus className="w-4 h-4 mr-1" /> New Reservation
</Button>
</div>

<div className="flex gap-1 mb-6 bg-gray-100 p-1 rounded-lg w-fit">
<button onClick={() => setTab("my")} className={`px-4 py-2 rounded-md text-sm font-medium ${
tab === "my" ? "bg-white shadow text-gray-900" : "text-gray-500"
}`}>My Reservations</button>
<button onClick={() => setTab("pending")} className={`px-4 py-2 rounded-md text-sm font-medium flex items-center gap-1.5 ${
tab === "pending" ? "bg-white shadow text-gray-900" : "text-gray-500"
}`}>
Pending Confirmation
{pendingConfirmations.length > 0 && (
<span className="bg-yellow-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">{pendingConfirmations.length}</span>
)}
</button>
</div>

<div className="space-y-3">
{currentList.length === 0 ? (
<div className="bg-white border rounded-xl p-12 text-center">
<Calendar className="w-12 h-12 text-gray-300 mx-auto mb-3" />
<p className="text-gray-500">No reservations</p>
</div>
) : (
currentList.map((r) => (
<div key={r.id} className="bg-white border rounded-xl p-4">
<div className="flex items-start justify-between">
<div>
<div className="flex items-center gap-2 mb-1">
<p className="font-medium text-gray-900">{r.assetName}</p>
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${STATUS_COLORS[r.status]}`}>{r.status}</span>
</div>
<p className="text-sm text-gray-500">{r.purpose}</p>
<p className="text-xs text-gray-400 mt-1">
{new Date(r.startsAt).toLocaleDateString()} {new Date(r.startsAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
{" — "}
{new Date(r.endsAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
{" · "}Requested by {r.requester}
</p>
</div>
<div className="flex gap-2">
{tab === "pending" && r.status === "PENDING" && (
<>
<Button size="sm" onClick={() => handleConfirm(r.id)}><Check className="w-3 h-3 mr-1" /> Confirm</Button>
<Button variant="outline" size="sm" onClick={() => handleCancel(r.id)}><X className="w-3 h-3 mr-1" /> Reject</Button>
</>
)}
{tab === "my" && r.status === "PENDING" && (
<Button variant="outline" size="sm" onClick={() => handleCancel(r.id)}>Cancel</Button>
)}
</div>
</div>
</div>
))
)}
</div>

{showBooking && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/40" onClick={() => setShowBooking(false)} />
<div className="relative bg-white rounded-xl shadow-xl p-6 w-full max-w-md">
<h3 className="font-semibold mb-4">New Reservation</h3>
<div className="space-y-3">
<Input placeholder="Asset ID" />
<Input placeholder="Purpose" />
<div className="grid grid-cols-2 gap-3">
<div><label className="text-xs text-gray-500">Start</label><Input type="datetime-local" /></div>
<div><label className="text-xs text-gray-500">End</label><Input type="datetime-local" /></div>
</div>
</div>
<div className="flex justify-end gap-2 mt-4">
<Button variant="outline" onClick={() => setShowBooking(false)}>Cancel</Button>
<Button onClick={() => setShowBooking(false)}>Submit Request</Button>
</div>
</div>
</div>
)}
</div>
);
}
2 changes: 2 additions & 0 deletions frontend/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const geistMono = Geist_Mono({
export const metadata: Metadata = {
title: "Assets Up",
description: "Modern Assets and Inventory Management Platform",
manifest: "/manifest.json",
themeColor: "#111827",
};

export default function RootLayout({
Expand Down
165 changes: 165 additions & 0 deletions frontend/components/assets/depreciation-schedule.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
"use client";

import { useState } from "react";
import { TrendingDown, DollarSign, BarChart3 } from "lucide-react";

interface DepreciationEntry {
period: number;
openingValue: number;
depreciation: number;
closingValue: number;
}

interface DepreciationData {
method: string;
usefulLifeMonths: number;
salvageValue: number;
purchaseCost: number;
currentValue: number;
schedule: DepreciationEntry[];
}

const MOCK_DATA: DepreciationData = {
method: "STRAIGHT_LINE",
usefulLifeMonths: 60,
salvageValue: 500,
purchaseCost: 5000,
currentValue: 3200,
schedule: Array.from({ length: 12 }, (_, i) => ({
period: i + 1,
openingValue: 5000 - i * 375,
depreciation: 375,
closingValue: 5000 - (i + 1) * 375,
})),
};

export function DepreciationSchedule({ data = MOCK_DATA }: { data?: DepreciationData }) {
const [expanded, setExpanded] = useState(false);
const visibleSchedule = expanded ? data.schedule : data.schedule.slice(0, 6);

return (
<div className="bg-white rounded-xl border border-gray-200 p-5">
<h3 className="text-sm font-semibold text-gray-900 mb-4">Depreciation Schedule</h3>

{/* Stats */}
<div className="grid grid-cols-3 gap-4 mb-4">
<div className="bg-gray-50 rounded-lg p-3">
<p className="text-xs text-gray-500">Purchase Cost</p>
<p className="text-lg font-semibold">${data.purchaseCost.toLocaleString()}</p>
</div>
<div className="bg-gray-50 rounded-lg p-3">
<p className="text-xs text-gray-500">Current Book Value</p>
<p className="text-lg font-semibold">${data.currentValue.toLocaleString()}</p>
</div>
<div className="bg-gray-50 rounded-lg p-3">
<p className="text-xs text-gray-500">Salvage Value</p>
<p className="text-lg font-semibold">${data.salvageValue.toLocaleString()}</p>
</div>
</div>

{/* Simple bar chart */}
<div className="mb-4">
<p className="text-xs text-gray-500 mb-2">Book Value Over Time</p>
<div className="flex items-end gap-1 h-32">
{data.schedule.map((entry) => {
const height = (entry.closingValue / data.purchaseCost) * 100;
return (
<div key={entry.period} className="flex-1 flex flex-col items-center gap-1">
<div
className="w-full bg-blue-500 rounded-t"
style={{ height: `${height}%` }}
title={`Period ${entry.period}: $${entry.closingValue.toLocaleString()}`}
/>
<span className="text-[10px] text-gray-400">{entry.period}</span>
</div>
);
})}
</div>
</div>

{/* Table */}
<div className={`${expanded ? "" : "max-h-48 overflow-hidden"}`}>
<table className="w-full text-sm">
<thead>
<tr className="border-b text-gray-500">
<th className="text-left py-2 font-medium">Period</th>
<th className="text-right py-2 font-medium">Opening</th>
<th className="text-right py-2 font-medium">Depreciation</th>
<th className="text-right py-2 font-medium">Closing</th>
</tr>
</thead>
<tbody>
{visibleSchedule.map((entry) => (
<tr key={entry.period} className="border-b border-gray-100">
<td className="py-2">{entry.period}</td>
<td className="py-2 text-right">${entry.openingValue.toLocaleString()}</td>
<td className="py-2 text-right text-red-600">-${entry.depreciation.toLocaleString()}</td>
<td className="py-2 text-right font-medium">${entry.closingValue.toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</div>
{data.schedule.length > 6 && (
<button onClick={() => setExpanded(!expanded)} className="text-sm text-blue-600 hover:text-blue-800 mt-2">
{expanded ? "Show less" : `Show all ${data.schedule.length} periods`}
</button>
)}
</div>
);
}

export function DepreciationFinancialsTab() {
return (
<div className="space-y-6">
<div className="grid grid-cols-3 gap-4">
<div className="bg-white border rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<DollarSign className="w-5 h-5 text-gray-400" />
<span className="text-sm text-gray-500">Total Purchase Value</span>
</div>
<p className="text-2xl font-bold">$125,000</p>
</div>
<div className="bg-white border rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<TrendingDown className="w-5 h-5 text-gray-400" />
<span className="text-sm text-gray-500">Current Book Value</span>
</div>
<p className="text-2xl font-bold">$87,500</p>
</div>
<div className="bg-white border rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<BarChart3 className="w-5 h-5 text-gray-400" />
<span className="text-sm text-gray-500">Monthly Depreciation</span>
</div>
<p className="text-2xl font-bold">$2,083</p>
</div>
</div>

{/* Category breakdown placeholder */}
<div className="bg-white border rounded-xl p-5">
<h3 className="text-sm font-semibold mb-4">Book Value by Category</h3>
<div className="space-y-3">
{[
{ name: "Electronics", value: 45000, pct: 51 },
{ name: "Furniture", value: 22000, pct: 25 },
{ name: "Vehicles", value: 15500, pct: 18 },
{ name: "Other", value: 5000, pct: 6 },
].map((cat) => (
<div key={cat.name}>
<div className="flex justify-between text-sm mb-1">
<span>{cat.name}</span>
<span className="text-gray-500">${cat.value.toLocaleString()}</span>
</div>
<div className="w-full bg-gray-100 rounded-full h-2">
<div className="bg-blue-500 h-2 rounded-full" style={{ width: `${cat.pct}%` }} />
</div>
</div>
))}
</div>
</div>

<DepreciationSchedule />
</div>
);
}
Loading
Loading