diff --git a/package-lock.json b/package-lock.json index 225352b..deb9c29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3503,7 +3503,7 @@ "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -3513,7 +3513,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -4796,7 +4796,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -9345,7 +9345,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/src/app/globals.css b/src/app/globals.css index 719b2cf..8cb2ba6 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -145,3 +145,12 @@ @apply bg-background text-foreground; } } + +/* Hide scrollbar for horizontal scroll containers */ +@utility scrollbar-hide { + -ms-overflow-style: none; + scrollbar-width: none; + &::-webkit-scrollbar { + display: none; + } +} diff --git a/src/app/loans/page.tsx b/src/app/loans/page.tsx index e62c9e1..5cff9f2 100644 --- a/src/app/loans/page.tsx +++ b/src/app/loans/page.tsx @@ -28,11 +28,11 @@ export default async function LoanDashboardPage() { const items = await getItems(); return ( -
+
-
+
-

Loans

+

Loans

{loans.length} LOAN REQUESTS

@@ -43,7 +43,7 @@ export default async function LoanDashboardPage() { />
-
+
diff --git a/src/components/DashboardNav.tsx b/src/components/DashboardNav.tsx index 6656bec..97fb588 100644 --- a/src/components/DashboardNav.tsx +++ b/src/components/DashboardNav.tsx @@ -13,12 +13,12 @@ interface DashboardNavProps { export function DashboardNav({ userRole }: DashboardNavProps) { const pathname = usePathname(); // Unauthenticated users only see Catalogue tab - const tabs = userRole - ? getAvailableTabs(userRole) + const tabs = userRole + ? getAvailableTabs(userRole) : [{ name: 'CATALOGUE', href: '/catalogue' }]; return ( -
+
{tabs.map((tab) => { const isActive = pathname.startsWith(tab.href); return ( @@ -26,7 +26,7 @@ export function DashboardNav({ userRole }: DashboardNavProps) { key={tab.name} href={tab.href} className={cn( - "pb-4 text-sm font-bold tracking-wide transition-colors hover:text-white", + "pb-3 md:pb-4 text-xs md:text-sm font-bold tracking-wide transition-colors hover:text-white whitespace-nowrap shrink-0", isActive ? "border-b-2 border-[#57A6FF] text-white" : "text-white/50 border-b-2 border-transparent" diff --git a/src/components/catalogue/Catalogue.tsx b/src/components/catalogue/Catalogue.tsx index a6e35d8..2837d7f 100644 --- a/src/components/catalogue/Catalogue.tsx +++ b/src/components/catalogue/Catalogue.tsx @@ -171,11 +171,11 @@ export default function Catalogue({ slocs, ihs, userRole }: CatalogueProps) { }, [fetchItems]); return ( -
+
-
+
-

Catalogue

+

Catalogue

{totalItems} ITEMS

{canEdit && ( @@ -183,8 +183,8 @@ export default function Catalogue({ slocs, ihs, userRole }: CatalogueProps) { )}
-
-
+
+
)}
-
- +
+ {/*Reset Filters Button - only show when filters are active */} {hasActiveFilters && ( )} - + setSortOption(e.target.value as SortOption)} - className="h-9 rounded-md bg-white/20 px-3 text-white border border-white/20 focus:outline-none" + className="h-8 md:h-9 rounded-md bg-white/20 px-2 md:px-3 text-sm text-white border border-white/20 focus:outline-none" > - - - + + + - + {/*Asc/Desc Button*/} @@ -303,7 +304,7 @@ export default function Catalogue({ slocs, ihs, userRole }: CatalogueProps) { {/* Action buttons - appear on hover (only for LOGS+) */} {canEdit && ( -
+
-
+
-
+
-
+
( - + ( - + + + + + + Navigation Menu + App navigation and user actions + +
+ {children} +
+
+
+
+ ); +} diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx index 24d48b6..17c87b7 100644 --- a/src/components/layout/Navbar.tsx +++ b/src/components/layout/Navbar.tsx @@ -4,12 +4,21 @@ import { getSession } from '@/lib/auth/session'; import { UserMenu } from '@/components/auth/UserMenu'; import { LoginModal } from '@/components/auth/LoginModal'; import { DevRoleDropdown } from '@/components/auth/DevRoleDropdown'; +import { MobileNav } from './MobileNav'; export async function Navbar() { const session = await getSession(); const botUsername = process.env.TELEGRAM_BOT_USERNAME; const isDev = process.env.NODE_ENV === 'development'; + const authContent = isDev ? ( + + ) : session ? ( + + ) : ( + botUsername && + ); + return ( ); diff --git a/src/components/loans/LoanFormModal.tsx b/src/components/loans/LoanFormModal.tsx index c651d15..1a4ca30 100644 --- a/src/components/loans/LoanFormModal.tsx +++ b/src/components/loans/LoanFormModal.tsx @@ -263,7 +263,7 @@ export function LoanFormModal({ {triggerElement} )} - + {/* Success State after creating */} {createdLoanRefNo !== null ? ( <> @@ -307,7 +307,7 @@ export function LoanFormModal({ {/* Requester Section - only for add mode */} {mode === "add" ? ( -
+

{form.watch("newRequester") ? "New Requester" : "Requester"} @@ -341,7 +341,7 @@ export function LoanFormModal({ />

) : loan && ( -
+

Requester

@@ -356,7 +356,7 @@ export function LoanFormModal({ )} {/* Loan Details */} -
+

Loan Details

{/* Items Section */} -
+

Items

totalQty; return ( -
-
-
{itemInfo?.itemDesc || "Unknown Item"}
+
+
+
{itemInfo?.itemDesc || "Unknown Item"}
{isOverLimit ? ( Exceeds total ({totalQty}) @@ -467,7 +467,7 @@ export function LoanFormModal({ )}
-
+
-
-
+ {/* Desktop Table */} +
@@ -291,15 +293,15 @@ export function LoansTable({ data, items }: LoansTableProps) { View details - + {loan.loanRequestStatus === LoanRequestStatus.PENDING && ( <> - + - { if (!isDeleting) { setDeleteDialogOpen(open); @@ -358,9 +360,117 @@ export function LoansTable({ data, items }: LoansTableProps) {
- {/* Details Modal */} + {/* Mobile Card View */} +
+ {filteredData.length === 0 ? ( +
+ No loans found. +
+ ) : ( + filteredData.map((loan) => ( +
setSelectedLoan(loan)} + > + {/* Top row: Ref + Status */} +
+ #{loan.refNo} + +
+ + {/* Requester */} +
+
+ {loan.requester.firstName}{loan.requester.lastName ? ` ${loan.requester.lastName}` : ''} +
+
@{loan.requester.telegramHandle}
+ {loan.organisation && ( +
{loan.organisation}
+ )} +
+ + {/* Items preview */} +
+ {loan.loanDetails.slice(0, 2).map((detail) => ( +
+ + {detail.item.itemDesc} + x{detail.loanQty} +
+ ))} + {loan.loanDetails.length > 2 && ( + +{loan.loanDetails.length - 2} more + )} +
+ + {/* Period */} +
+ + {format(new Date(loan.loanDateStart), "dd MMM")} - {format(new Date(loan.loanDateEnd), "dd MMM")} + {loan.loanRequestStatus === LoanRequestStatus.ONGOING && new Date() > new Date(loan.loanDateEnd) && ( + Overdue + )} +
+ + {/* Actions for pending loans */} + {loan.loanRequestStatus === LoanRequestStatus.PENDING && ( +
e.stopPropagation()}> + + { + if (!isDeleting) { + setDeleteDialogOpen(open); + if (open) setDeletingRefNo(loan.refNo); + } + }} + > + + + + + + Delete Loan Request + + Are you sure you want to delete loan #{loan.refNo}? This action cannot be undone. + + + + Cancel + + + + +
+ )} +
+ )) + )} +
+ + {/* Details Modal — Item 5: responsive */} !open && setSelectedLoan(null)}> - +
Loan Details #{selectedLoan?.refNo} @@ -372,17 +482,17 @@ export function LoansTable({ data, items }: LoansTableProps) {
-
+
Requester -
{selectedLoan?.requester.firstName}{selectedLoan?.requester.lastName ? ` ${selectedLoan.requester.lastName}` : ''}
+
{selectedLoan?.requester.firstName}{selectedLoan?.requester.lastName ? ` ${selectedLoan.requester.lastName}` : ''}
{selectedLoan?.requester.nusnetId && (
{selectedLoan.requester.nusnetId}
)}
Period -
+
{selectedLoan && format(new Date(selectedLoan.loanDateStart), "dd MMM")} - {selectedLoan && format(new Date(selectedLoan.loanDateEnd), "dd MMM yyyy")}
{/* Lateness Check */} @@ -397,21 +507,21 @@ export function LoansTable({ data, items }: LoansTableProps) { {/* Approval Actions */} {selectedLoan?.loanRequestStatus === LoanRequestStatus.PENDING && ( canApproveLoan(selectedLoan) ? ( -
+
This request is Pending Approval. Approving will mark items as on loan.
-
+
) : ( -
+
Cannot approve: Some items have insufficient available stock. See details below.
-
+
@@ -419,7 +529,8 @@ export function LoansTable({ data, items }: LoansTableProps) { ) )} -
+ {/* Items — desktop table */} +
@@ -436,7 +547,7 @@ export function LoansTable({ data, items }: LoansTableProps) { {selectedLoan?.loanDetails.map((detail) => { const { available, sufficient } = getItemAvailability(detail.itemId, detail.loanQty); const showAvailability = selectedLoan.loanRequestStatus === LoanRequestStatus.PENDING; - + return ( @@ -475,6 +586,50 @@ export function LoansTable({ data, items }: LoansTableProps) {
+ + {/* Items — mobile card list */} +
+ {selectedLoan?.loanDetails.map((detail) => { + const { available, sufficient } = getItemAvailability(detail.itemId, detail.loanQty); + const showAvailability = selectedLoan.loanRequestStatus === LoanRequestStatus.PENDING; + + return ( +
+
+
+
{detail.item.itemDesc}
+
ID: {detail.itemId}
+
+ +
+
+
+ Qty: {detail.loanQty} + {showAvailability && ( + + {" "}(avail: {available}{!sufficient ? " — insufficient" : ""}) + + )} +
+ {selectedLoan.loanRequestStatus === LoanRequestStatus.ONGOING && detail.loanItemStatus === LoanItemStatus.ON_LOAN && ( + + )} +
+
+ ); + })} +
@@ -513,4 +668,3 @@ function ItemStatusBadge({ status }: { status: LoanItemStatus }) { return {status}; } } - diff --git a/src/components/loans/RequesterSelector.tsx b/src/components/loans/RequesterSelector.tsx index 87f4f66..46d92fc 100644 --- a/src/components/loans/RequesterSelector.tsx +++ b/src/components/loans/RequesterSelector.tsx @@ -82,7 +82,7 @@ export function RequesterSelector({ requesters, value, onChange, onNewDetailsCha className="space-y-3" onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()} > -
+
handleNewChange("lastName", e.target.value)} placeholder="Doe" />
-
+
@@ -262,22 +263,22 @@ export function GroupsView({ groups, users, onRefresh, canManage = false }: Grou return (
-
-
-
- +
+
+
+ {fullName} {member.isPrimary && ( - Primary POC + Primary POC )}
-
+
@{member.user.telegramHandle}
@@ -387,7 +388,7 @@ function AddMemberPopover({ {resolvedTrigger} - + diff --git a/src/components/users/ManageUsers.tsx b/src/components/users/ManageUsers.tsx index de30f59..a697d0e 100644 --- a/src/components/users/ManageUsers.tsx +++ b/src/components/users/ManageUsers.tsx @@ -32,12 +32,12 @@ export default function ManageUsers({ users, groupIHs, userRole, canManage }: Ma }, [router]); return ( -
+
{/* Header */} -
+
-

Manage Users

+

Manage Users

{viewMode === "individual" ? `${users.length} user${users.length !== 1 ? "s" : ""}` diff --git a/src/components/users/UsersTable.tsx b/src/components/users/UsersTable.tsx index 07e943a..31a687c 100644 --- a/src/components/users/UsersTable.tsx +++ b/src/components/users/UsersTable.tsx @@ -130,141 +130,210 @@ export function UsersTable({ users, groups, onRefresh, canManage = false, actorR ); } + const DeleteButton = ({ user }: { user: UserWithDetails }) => ( + + + { + if (!isDeleting) { + setDeleteDialogOpen(open); + if (open) setDeletingId(user.userId); + } + }} + > + + + + + + + + + + Delete User + + Are you sure you want to delete{" "} + {getFullName(user)}? This action + cannot be undone. + + + + Cancel + + + + + +

{getDeleteDisabledReason(user)}

+ + + + ); + return ( -
- - - - Name - Telegram - NUSNET - Role - Groups - {canManage && Actions} - - - - {users.map((user) => ( - - - {getFullName(user)} - - - @ - {user.telegramHandle} - - - {user.nusnetId || ( - - + <> + {/* Desktop Table */} +
+
+ + + Name + Telegram + NUSNET + Role + Groups + {canManage && Actions} + + + + {users.map((user) => ( + + + {getFullName(user)} + + + @ + {user.telegramHandle} + + + {user.nusnetId || ( + - + )} + + + + {user.role} + + + + {getUserGroups(user).length > 0 ? ( +
+ {getUserGroups(user).slice(0, 2).map((group) => ( + + {group.ihName} + + ))} + {getUserGroups(user).length > 2 && ( + + +{getUserGroups(user).length - 2} + + )} +
+ ) : ( + - + )} +
+ {canManage && ( + +
+ {canEditUser(user) && ( + + )} + +
+
)} - - +
+ ))} +
+
+
+ + {/* Mobile Card View */} +
+ {users.map((user) => { + const userGroups = getUserGroups(user); + return ( +
+ {/* Name + Role */} +
+
+
{getFullName(user)}
+
@{user.telegramHandle}
+
{user.role} - - - {getUserGroups(user).length > 0 ? ( -
- {getUserGroups(user).slice(0, 2).map((group) => ( - - {group.ihName} - - ))} - {getUserGroups(user).length > 2 && ( - - +{getUserGroups(user).length - 2} - - )} -
- ) : ( - - - )} -
+
+ + {/* NUSNET */} + {user.nusnetId && ( +
+ NUSNET: {user.nusnetId} +
+ )} + + {/* Groups */} + {userGroups.length > 0 && ( +
+ {userGroups.map((group) => ( + + {group.ihName} + + ))} +
+ )} + + {/* Actions */} {canManage && ( - -
- {canEditUser(user) && ( - - )} - - - { - if (!isDeleting) { - setDeleteDialogOpen(open); - if (open) setDeletingId(user.userId); - } - }} - > - - - - - - - - - - Delete User - - Are you sure you want to delete{" "} - {getFullName(user)}? This action - cannot be undone. - - - - Cancel - - - - - -

{getDeleteDisabledReason(user)}

-
-
-
-
-
+
+ {canEditUser(user) && ( + + )} + +
)} - - ))} - - -
+
+ ); + })} +
+ ); } diff --git a/tsconfig.json b/tsconfig.json index a29dde2..2c24f6f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", + "jsx": "react-jsx", "incremental": true, "plugins": [ { @@ -40,4 +40,4 @@ "exclude": [ "node_modules" ] -} \ No newline at end of file +}