Initial commit aplikasi inventory
This commit is contained in:
1 parent
5c97539b46
commit
8a04f517da
9 files changed
+2315
-269
No files matched your search
BIN
Binary file not shown.
+15
-1
@@ -1,7 +1,21 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
// @ts-ignore - allow testing via LAN IP
|
||||
allowedDevOrigins: ['192.168.80.50'],
|
||||
output: 'standalone',
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: '/(.*)',
|
||||
headers: [
|
||||
{ key: 'X-Frame-Options', value: 'DENY' },
|
||||
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
||||
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
Generated
+1853
-35
File diff suppressed because it is too large.
Load diff
@@ -9,16 +9,24 @@
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@libsql/client": "^0.17.4",
|
||||
"@prisma/adapter-libsql": "^7.9.1",
|
||||
"@prisma/client": "^7.9.1",
|
||||
"jose": "^6.2.8",
|
||||
"next": "16.3.0",
|
||||
"nodemailer": "^9.0.4",
|
||||
"react": "19.2.8",
|
||||
"react-dom": "19.2.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/nodemailer": "^8.0.1",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"dotenv": "^17.4.2",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.3.0",
|
||||
"prisma": "^7.9.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
+113
-33
@@ -1,34 +1,17 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
--bg-color: #0f172a;
|
||||
--glass-bg: rgba(30, 41, 59, 0.7);
|
||||
--glass-border: rgba(255, 255, 255, 0.1);
|
||||
--text-main: #f8fafc;
|
||||
--text-muted: #94a3b8;
|
||||
--accent: #3b82f6;
|
||||
--accent-hover: #2563eb;
|
||||
--danger: #ef4444;
|
||||
--success: #10b981;
|
||||
--warning: #f59e0b;
|
||||
--sold: #8b5cf6;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -37,13 +20,110 @@ body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-main);
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-image:
|
||||
radial-gradient(circle at 15% 50%, rgba(59, 130, 246, 0.15), transparent 25%),
|
||||
radial-gradient(circle at 85% 30%, rgba(16, 185, 129, 0.15), transparent 25%);
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color-scheme: dark;
|
||||
.glass-panel {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 8px;
|
||||
color: var(--text-main);
|
||||
font-family: 'Inter', sans-serif;
|
||||
transition: border-color 0.2s ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
option {
|
||||
background-color: #1e293b;
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.025em;
|
||||
background: linear-gradient(135deg, var(--text-main) 0%, #a1a1aa 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.page-grid {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.page-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.subheading {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
+45
-19
@@ -1,26 +1,52 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import type { Metadata } from 'next'
|
||||
import { Inter } from 'next/font/google'
|
||||
import './globals.css'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import Navbar from '@/components/Navbar'
|
||||
import AutoRefresh from '@/components/AutoRefresh'
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
title: 'Portal Inventory',
|
||||
description: 'Modern Inventory Management System',
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }: LayoutProps<"/">) {
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const session = await getSession()
|
||||
let pendingCount = 0
|
||||
if (session?.role === 'approver') {
|
||||
const { prisma } = await import('@/lib/prisma')
|
||||
const user = await prisma.user.findUnique({ where: { id: session.id }, include: { categories: true } })
|
||||
const allowedCategoryIds = user?.categories.map(c => c.id) || []
|
||||
const requests = await prisma.request.findMany({
|
||||
where: {
|
||||
status: { in: ['pending', 'approved_1'] },
|
||||
item: { categoryId: { in: allowedCategoryIds } }
|
||||
},
|
||||
include: { approvalLogs: true }
|
||||
})
|
||||
pendingCount = requests.filter(r => {
|
||||
if (r.userId === session.id) return false // Cannot approve own requests
|
||||
if (r.status === 'pending') return true
|
||||
const isOneStep = ['purchase', 'return_borrow', 'return_trial', 'return_billed'].includes(r.type)
|
||||
if (isOneStep) return false
|
||||
const firstApproverId = r.approvalLogs.find(l => l.approvalStep === 1)?.approverId
|
||||
return firstApproverId !== session.id
|
||||
}).length
|
||||
}
|
||||
|
||||
return (
|
||||
<html lang="en" className={`${geistSans.variable} ${geistMono.variable}`}>
|
||||
<body>{children}</body>
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
{session && <Navbar role={session.role} name={session.name} pendingCount={pendingCount} />}
|
||||
{session && session.role === 'approver' && <AutoRefresh interval={5000} pendingCount={pendingCount} />}
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
)
|
||||
}
|
||||
+100
-118
@@ -1,150 +1,132 @@
|
||||
.page {
|
||||
--background: #fafafa;
|
||||
--foreground: #fff;
|
||||
|
||||
--text-primary: #000;
|
||||
--text-secondary: #666;
|
||||
|
||||
--button-primary-hover: #383838;
|
||||
--button-secondary-hover: #f2f2f2;
|
||||
--button-secondary-border: #ebebeb;
|
||||
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--font-geist-sans);
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
padding: 1rem 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
background-color: var(--foreground);
|
||||
padding: 120px 60px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
gap: 24px;
|
||||
.header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.intro h1 {
|
||||
max-width: 320px;
|
||||
font-size: 40px;
|
||||
.metricsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 1rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.metricsGrid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.metricCard {
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.metricCard:hover {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.metricCard h3 {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
line-height: 48px;
|
||||
letter-spacing: -2.4px;
|
||||
text-wrap: balance;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.intro p {
|
||||
max-width: 440px;
|
||||
font-size: 18px;
|
||||
line-height: 32px;
|
||||
text-wrap: balance;
|
||||
color: var(--text-secondary);
|
||||
.metricValue {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.intro a {
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
.textSuccess { color: var(--success); }
|
||||
.textWarning { color: var(--warning); }
|
||||
.textAccent { color: var(--accent); }
|
||||
.textMuted { color: var(--text-muted); }
|
||||
.textDanger { color: var(--danger); }
|
||||
.textSold { color: var(--sold); }
|
||||
|
||||
.tableContainer {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.ctas {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
.table {
|
||||
width: 100%;
|
||||
max-width: 440px;
|
||||
gap: 16px;
|
||||
font-size: 14px;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ctas a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
border-radius: 128px;
|
||||
border: 1px solid transparent;
|
||||
transition: 0.2s;
|
||||
cursor: pointer;
|
||||
width: fit-content;
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.table th {
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.itemName {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
a.primary {
|
||||
background: var(--text-primary);
|
||||
color: var(--background);
|
||||
gap: 8px;
|
||||
.badgeSuccess {
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
color: var(--success);
|
||||
padding: 4px 10px;
|
||||
border-radius: 9999px;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
a.secondary {
|
||||
border-color: var(--button-secondary-border);
|
||||
.badgeWarning {
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
color: var(--warning);
|
||||
padding: 4px 10px;
|
||||
border-radius: 9999px;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Enable hover only on non-touch devices */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
a.primary:hover {
|
||||
background: var(--button-primary-hover);
|
||||
border-color: transparent;
|
||||
.badgeAccent {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: var(--accent);
|
||||
padding: 4px 10px;
|
||||
border-radius: 9999px;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
a.secondary:hover {
|
||||
background: var(--button-secondary-hover);
|
||||
border-color: transparent;
|
||||
}
|
||||
.clickableCard {
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.main {
|
||||
padding: 48px 24px;
|
||||
.clickableCard:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.intro {
|
||||
gap: 16px;
|
||||
.linkWrapper {
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.intro h1 {
|
||||
font-size: 32px;
|
||||
line-height: 40px;
|
||||
letter-spacing: -1.92px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.logo {
|
||||
filter: invert();
|
||||
}
|
||||
|
||||
.page {
|
||||
--background: #000;
|
||||
--foreground: #000;
|
||||
|
||||
--text-primary: #ededed;
|
||||
--text-secondary: #999;
|
||||
|
||||
--button-primary-hover: #ccc;
|
||||
--button-secondary-hover: #1a1a1a;
|
||||
--button-secondary-border: #1a1a1a;
|
||||
}
|
||||
}
|
||||
|
||||
.code {
|
||||
font-family: var(--font-geist-mono);
|
||||
font-size: 0.9em;
|
||||
background: color-mix(in srgb, currentColor 8%, transparent);
|
||||
padding: 0.1em 0.4em;
|
||||
border-radius: 6px;
|
||||
.linkWrapper .metricCard {
|
||||
height: 100%;
|
||||
}
|
||||
+181
-62
@@ -1,69 +1,188 @@
|
||||
import Image from "next/image";
|
||||
import styles from "./page.module.css";
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import styles from './page.module.css'
|
||||
import Link from 'next/link'
|
||||
import { Category } from '@prisma/client'
|
||||
|
||||
export default function Home() {
|
||||
export default async function Dashboard({ searchParams }: { searchParams: Promise<{ group?: string }> }) {
|
||||
const { group } = await searchParams
|
||||
const session = await getSession()
|
||||
const user = session ? await prisma.user.findUnique({ where: { id: session.id }, include: { categories: true } }) : null
|
||||
|
||||
let allowedCategories: Category[] = []
|
||||
if (session?.role === 'super_admin') {
|
||||
allowedCategories = await prisma.category.findMany()
|
||||
} else if (user) {
|
||||
allowedCategories = user.categories
|
||||
}
|
||||
|
||||
let displayCategories = allowedCategories
|
||||
if (group && group !== 'all') {
|
||||
const groupId = parseInt(group)
|
||||
displayCategories = allowedCategories.filter(c => c.id === groupId)
|
||||
}
|
||||
|
||||
const allowedCategoryIds = displayCategories.map(c => c.id)
|
||||
|
||||
const items = await prisma.item.findMany({
|
||||
where: { categoryId: { in: allowedCategoryIds } },
|
||||
include: { category: true }
|
||||
})
|
||||
|
||||
const groupedData = displayCategories.map(cat => {
|
||||
const catItems = items.filter(i => i.categoryId === cat.id)
|
||||
const metrics = catItems.reduce(
|
||||
(acc, item) => {
|
||||
acc.total += item.totalQuantity
|
||||
acc.borrowed += item.borrowedQty
|
||||
acc.trial += item.trialQty
|
||||
acc.billed += item.billedQty
|
||||
acc.available += item.availableQty
|
||||
acc.damaged += item.damagedQty || 0
|
||||
acc.sold += item.soldQty || 0
|
||||
return acc
|
||||
},
|
||||
{ total: 0, borrowed: 0, trial: 0, billed: 0, available: 0, damaged: 0, sold: 0 }
|
||||
)
|
||||
return { category: cat, items: catItems, metrics }
|
||||
})
|
||||
|
||||
// Also catch uncategorized items if they somehow exist and the user is super_admin (and viewing all or uncategorized)
|
||||
const showUncategorized = session?.role === 'super_admin' && (!group || group === 'all' || group === '0')
|
||||
const uncategorizedItems = showUncategorized ? await prisma.item.findMany({ where: { categoryId: null } }) : []
|
||||
if (uncategorizedItems.length > 0) {
|
||||
const metrics = uncategorizedItems.reduce(
|
||||
(acc, item) => {
|
||||
acc.total += item.totalQuantity
|
||||
acc.borrowed += item.borrowedQty
|
||||
acc.trial += item.trialQty
|
||||
acc.billed += item.billedQty
|
||||
acc.available += item.availableQty
|
||||
acc.damaged += item.damagedQty || 0
|
||||
acc.sold += item.soldQty || 0
|
||||
return acc
|
||||
},
|
||||
{ total: 0, borrowed: 0, trial: 0, billed: 0, available: 0, damaged: 0, sold: 0 }
|
||||
)
|
||||
groupedData.push({ category: { id: 0, name: 'Uncategorized', createdAt: new Date(), updatedAt: new Date() }, items: uncategorizedItems as any[], metrics })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<main className={styles.main}>
|
||||
<Image
|
||||
className={styles.logo}
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={100}
|
||||
height={20}
|
||||
priority
|
||||
/>
|
||||
<div className={styles.intro}>
|
||||
<h1>
|
||||
To get started, edit the{" "}
|
||||
<code className={styles.code}>page.tsx</code> file.
|
||||
</h1>
|
||||
<p>
|
||||
Looking for a starting point or more instructions? Head over to{" "}
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Templates
|
||||
</a>{" "}
|
||||
or the{" "}
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learning
|
||||
</a>{" "}
|
||||
center.
|
||||
</p>
|
||||
<header className={styles.header}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '1rem' }}>
|
||||
<div>
|
||||
<h1 className="heading">Dashboard Overview</h1>
|
||||
<p className="subheading">Welcome back, {session?.name} ({session?.role})</p>
|
||||
</div>
|
||||
<div className={styles.ctas}>
|
||||
<a
|
||||
className={styles.primary}
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className={styles.logo}
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={16}
|
||||
height={14}
|
||||
/>
|
||||
Deploy Now
|
||||
</a>
|
||||
<a
|
||||
className={styles.secondary}
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
{allowedCategories.length > 1 && (
|
||||
<form method="get" style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
|
||||
<select name="group" className="input-field" defaultValue={group || 'all'} style={{ padding: '0.5rem 1rem', minWidth: '200px' }}>
|
||||
<option value="all">All Groups</option>
|
||||
{allowedCategories.map(c => (
|
||||
<option key={c.id} value={c.id}>{c.name}</option>
|
||||
))}
|
||||
{session?.role === 'super_admin' && <option value="0">Uncategorized</option>}
|
||||
</select>
|
||||
<button type="submit" className="btn-primary" style={{ padding: '0.5rem 1rem' }}>Filter</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{groupedData.length === 0 && (
|
||||
<div className="glass-panel" style={{ padding: '2rem', textAlign: 'center', color: 'var(--text-muted)' }}>
|
||||
You do not have access to any categories yet. Please contact your Super Admin.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{groupedData.map(group => (
|
||||
<div key={group.category.id} style={{ marginBottom: '3rem' }}>
|
||||
<h2 className="heading" style={{ fontSize: '1.5rem', marginBottom: '1rem', color: 'var(--accent)' }}>
|
||||
Group: {group.category.name}
|
||||
</h2>
|
||||
|
||||
<div className={styles.metricsGrid}>
|
||||
<div className={`glass-panel ${styles.metricCard}`}>
|
||||
<h3>Total Items</h3>
|
||||
<p className={styles.metricValue}>{group.metrics.total}</p>
|
||||
</div>
|
||||
<div className={`glass-panel ${styles.metricCard}`}>
|
||||
<h3>Available</h3>
|
||||
<p className={`${styles.metricValue} ${styles.textSuccess}`}>{group.metrics.available}</p>
|
||||
</div>
|
||||
<Link href={`/approvals?typeFilter=borrow&historyGroup=${group.category.id}`} className={styles.linkWrapper}>
|
||||
<div className={`glass-panel ${styles.metricCard} ${styles.clickableCard}`}>
|
||||
<h3>Borrowed</h3>
|
||||
<p className={`${styles.metricValue} ${styles.textWarning}`}>{group.metrics.borrowed}</p>
|
||||
</div>
|
||||
</Link>
|
||||
<Link href={`/approvals?typeFilter=trial&historyGroup=${group.category.id}`} className={styles.linkWrapper}>
|
||||
<div className={`glass-panel ${styles.metricCard} ${styles.clickableCard}`}>
|
||||
<h3>Trial</h3>
|
||||
<p className={`${styles.metricValue} ${styles.textAccent}`}>{group.metrics.trial}</p>
|
||||
</div>
|
||||
</Link>
|
||||
<Link href={`/approvals?typeFilter=billed&historyGroup=${group.category.id}`} className={styles.linkWrapper}>
|
||||
<div className={`glass-panel ${styles.metricCard} ${styles.clickableCard}`}>
|
||||
<h3>Manage Service</h3>
|
||||
<p className={`${styles.metricValue} ${styles.textMuted}`}>{group.metrics.billed}</p>
|
||||
</div>
|
||||
</Link>
|
||||
<Link href={`/approvals?typeFilter=damaged&historyGroup=${group.category.id}`} className={styles.linkWrapper}>
|
||||
<div className={`glass-panel ${styles.metricCard} ${styles.clickableCard}`}>
|
||||
<h3>Damaged</h3>
|
||||
<p className={`${styles.metricValue} ${styles.textDanger}`}>{group.metrics.damaged}</p>
|
||||
</div>
|
||||
</Link>
|
||||
<Link href={`/approvals?typeFilter=outright_purchase&historyGroup=${group.category.id}`} className={styles.linkWrapper}>
|
||||
<div className={`glass-panel ${styles.metricCard} ${styles.clickableCard}`}>
|
||||
<h3>Sold</h3>
|
||||
<p className={`${styles.metricValue} ${styles.textSold}`}>{group.metrics.sold}</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className={`glass-panel ${styles.tableContainer}`} style={{ marginTop: '1.5rem' }}>
|
||||
<h3 className="heading" style={{ padding: '1rem', borderBottom: '1px solid var(--glass-border)', fontSize: '1.1rem' }}>Inventory Details - {group.category.name}</h3>
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item Name</th>
|
||||
<th>Total Qty</th>
|
||||
<th>Available</th>
|
||||
<th>Borrowed</th>
|
||||
<th>Trial</th>
|
||||
<th>Manage Service</th>
|
||||
<th>Damaged</th>
|
||||
<th>Sold</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{group.items.map((item) => (
|
||||
<tr key={item.id}>
|
||||
<td className={styles.itemName}>{item.name}</td>
|
||||
<td>{item.totalQuantity}</td>
|
||||
<td><span className={styles.badgeSuccess}>{item.availableQty}</span></td>
|
||||
<td>{item.borrowedQty > 0 ? <span className={styles.badgeWarning}>{item.borrowedQty}</span> : 0}</td>
|
||||
<td>{item.trialQty > 0 ? <span className={styles.badgeAccent}>{item.trialQty}</span> : 0}</td>
|
||||
<td>{item.billedQty}</td>
|
||||
<td>{item.damagedQty > 0 ? <span className={styles.badgeDanger} style={{ color: 'var(--danger)', fontWeight: 'bold' }}>{item.damagedQty}</span> : 0}</td>
|
||||
<td>{item.soldQty}</td>
|
||||
</tr>
|
||||
))}
|
||||
{group.items.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={6} style={{ padding: '1rem', textAlign: 'center', color: 'var(--text-muted)' }}>
|
||||
No items in this category.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user