diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 77fd326..d015ef6 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -21,7 +21,8 @@ model User {
requests Request[]
approvals ApprovalLog[]
activityLogs ActivityLog[]
- categories Category[]
+ divisionId Int?
+ division Division? @relation(fields: [divisionId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
@@ -43,11 +44,21 @@ model SystemSettings {
updatedAt DateTime @updatedAt
}
+model Division {
+ id Int @id @default(autoincrement())
+ name String @unique
+ categories Category[]
+ users User[]
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+}
+
model Category {
id Int @id @default(autoincrement())
- name String @unique
- users User[]
- items Item[]
+ name String @unique
+ divisionId Int?
+ division Division? @relation(fields: [divisionId], references: [id])
+ items Item[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
diff --git a/src/app/admin/categories/page.tsx b/src/app/admin/categories/page.tsx
index c0e68b3..2e622b3 100644
--- a/src/app/admin/categories/page.tsx
+++ b/src/app/admin/categories/page.tsx
@@ -10,7 +10,8 @@ export default async function AdminCategoriesPage({ searchParams }: { searchPara
redirect('/')
}
- const categories = await prisma.category.findMany({ include: { _count: { select: { items: true, users: true } } } })
+ const categories = await prisma.category.findMany({ include: { division: true, _count: { select: { items: true } } } })
+ const divisions = await prisma.division.findMany()
const editingCategory = edit ? categories.find(c => c.id === parseInt(edit)) : null
async function addCategory(formData: FormData) {
@@ -26,7 +27,10 @@ export default async function AdminCategoriesPage({ searchParams }: { searchPara
redirect('/admin/categories?error=Category+already+exists')
}
- await prisma.category.create({ data: { name } })
+ const divisionIdStr = formData.get('divisionId') as string
+ const divisionId = divisionIdStr ? parseInt(divisionIdStr) : null
+
+ await prisma.category.create({ data: { name, divisionId } })
revalidatePath('/admin/categories')
redirect('/admin/categories')
}
@@ -50,9 +54,12 @@ export default async function AdminCategoriesPage({ searchParams }: { searchPara
}
}
+ const divisionIdStr = formData.get('divisionId') as string
+ const divisionId = divisionIdStr ? parseInt(divisionIdStr) : null
+
await prisma.category.update({
where: { id },
- data: { name }
+ data: { name, divisionId }
})
revalidatePath('/admin/categories')
@@ -62,10 +69,10 @@ export default async function AdminCategoriesPage({ searchParams }: { searchPara
async function deleteCategory(formData: FormData) {
'use server'
const id = parseInt(formData.get('id') as string)
- const categoryToDelete = await prisma.category.findUnique({ where: { id }, include: { _count: { select: { items: true, users: true } } } })
+ const categoryToDelete = await prisma.category.findUnique({ where: { id }, include: { _count: { select: { items: true } } } })
if (categoryToDelete) {
- if (categoryToDelete._count.items > 0 || categoryToDelete._count.users > 0) {
- redirect('/admin/categories?error=Cannot+delete+category+with+items+or+users')
+ if (categoryToDelete._count.items > 0) {
+ redirect('/admin/categories?error=Cannot+delete+category+with+items')
}
await prisma.category.delete({ where: { id } })
}
@@ -97,6 +104,15 @@ export default async function AdminCategoriesPage({ searchParams }: { searchPara
+
| #{category.id} |
{category.name} |
+ {category.division?.name || '-'} |
{category._count.items} |
- {category._count.users} |
diff --git a/src/app/admin/divisions/page.tsx b/src/app/admin/divisions/page.tsx
new file mode 100644
index 0000000..0ce5e55
--- /dev/null
+++ b/src/app/admin/divisions/page.tsx
@@ -0,0 +1,126 @@
+import { prisma } from '@/lib/prisma'
+import { getSession } from '@/lib/auth'
+import { redirect } from 'next/navigation'
+import { revalidatePath } from 'next/cache'
+
+export default async function DivisionsPage({ searchParams }: { searchParams: Promise<{ edit?: string }> }) {
+ const session = await getSession()
+ if (!session || session.role !== 'super_admin') {
+ redirect('/')
+ }
+ const sp = await searchParams
+
+ const divisions = await prisma.division.findMany({
+ include: {
+ categories: true,
+ users: true
+ },
+ orderBy: { createdAt: 'desc' }
+ })
+
+ let editingDivision = null
+ if (sp.edit) {
+ editingDivision = divisions.find(d => d.id === parseInt(sp.edit!))
+ }
+
+ async function addDivision(formData: FormData) {
+ 'use server'
+ const name = formData.get('name') as string
+ if (name) {
+ await prisma.division.create({ data: { name } })
+ }
+ revalidatePath('/admin/divisions')
+ }
+
+ async function updateDivision(formData: FormData) {
+ 'use server'
+ const id = parseInt(formData.get('id') as string)
+ const name = formData.get('name') as string
+ if (id && name) {
+ await prisma.division.update({ where: { id }, data: { name } })
+ }
+ revalidatePath('/admin/divisions')
+ redirect('/admin/divisions')
+ }
+
+ async function deleteDivision(formData: FormData) {
+ 'use server'
+ const id = parseInt(formData.get('id') as string)
+ if (id) {
+ // Must not delete if categories or users are attached, but for simplicity we will just try catch
+ try {
+ await prisma.division.delete({ where: { id } })
+ } catch (e) {
+ // Ignored for now, usually should alert user
+ }
+ }
+ revalidatePath('/admin/divisions')
+ }
+
+ return (
+
+ Division Management
+
+
+
+
+
+
+ | Division Name |
+ Categories Count |
+ Users Count |
+ Actions |
+
+
+
+ {divisions.map(div => (
+
+ | {div.name} |
+ {div.categories.length} |
+ {div.users.length} |
+
+ Edit
+
+ |
+
+ ))}
+
+
+
+
+
+ {editingDivision ? (
+ <>
+ Edit Division
+
+ >
+ ) : (
+ <>
+ Add New Division
+
+ >
+ )}
+
+
+
+ )
+}
diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx
index f22e5cc..6352ad0 100644
--- a/src/app/admin/users/page.tsx
+++ b/src/app/admin/users/page.tsx
@@ -3,15 +3,15 @@ import { getSession } from '@/lib/auth'
import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'
-export default async function AdminUsersPage({ searchParams }: { searchParams: Promise<{ edit?: string, error?: string, err_name?: string, err_email?: string, err_username?: string, err_password?: string, err_categories?: string, name?: string, email?: string, username?: string, role?: string }> }) {
- const { edit, error, err_name, err_email, err_username, err_password, err_categories, name: spName, email: spEmail, username: spUsername, role: spRole } = await searchParams
+export default async function AdminUsersPage({ searchParams }: { searchParams: Promise<{ edit?: string, error?: string, err_name?: string, err_email?: string, err_username?: string, err_password?: string, err_division?: string, err_categories?: string, name?: string, email?: string, username?: string, role?: string }> }) {
+ const { edit, error, err_name, err_email, err_username, err_password, err_division, name: spName, email: spEmail, username: spUsername, role: spRole } = await searchParams
const session = await getSession()
if (!session || session.role !== 'super_admin') {
redirect('/')
}
- const users = await prisma.user.findMany({ include: { categories: true } })
- const categories = await prisma.category.findMany()
+ const users = await prisma.user.findMany({ include: { division: true } })
+ const divisions = await prisma.division.findMany()
const editingUser = edit ? users.find(u => u.id === parseInt(edit)) : null
async function addUser(formData: FormData) {
@@ -21,15 +21,16 @@ export default async function AdminUsersPage({ searchParams }: { searchParams: P
const username = formData.get('username') as string
const password = formData.get('password') as string
const role = formData.get('role') as string
- const categoryIds = formData.getAll('categories').map(id => parseInt(id as string)).filter(id => !isNaN(id))
+ const divisionIdStr = formData.get('divisionId') as string
+ const divisionId = divisionIdStr ? parseInt(divisionIdStr) : null
const qs = `&name=${encodeURIComponent(name||'')}&email=${encodeURIComponent(email||'')}&username=${encodeURIComponent(username||'')}&role=${encodeURIComponent(role||'')}`
let hasError = false;
- let errName = '', errEmail = '', errUsername = '', errPassword = '', errCategories = '';
+ let errName = '', errEmail = '', errUsername = '', errPassword = '', errDivision = '';
- if (categoryIds.length === 0) {
- errCategories = 'At least one group/category is required';
+ if (!divisionId && role !== 'super_admin') {
+ errDivision = 'Division is required';
hasError = true;
}
@@ -66,7 +67,7 @@ export default async function AdminUsersPage({ searchParams }: { searchParams: P
}
if (hasError) {
- redirect(`/admin/users?err_name=${encodeURIComponent(errName)}&err_email=${encodeURIComponent(errEmail)}&err_username=${encodeURIComponent(errUsername)}&err_password=${encodeURIComponent(errPassword)}&err_categories=${encodeURIComponent(errCategories)}${qs}`)
+ redirect(`/admin/users?err_name=${encodeURIComponent(errName)}&err_email=${encodeURIComponent(errEmail)}&err_username=${encodeURIComponent(errUsername)}&err_password=${encodeURIComponent(errPassword)}&err_division=${encodeURIComponent(errDivision)}${qs}`)
}
await prisma.user.create({
@@ -76,7 +77,7 @@ export default async function AdminUsersPage({ searchParams }: { searchParams: P
username: username || null,
password,
role,
- categories: { connect: categoryIds.map(id => ({ id })) }
+ divisionId
}
})
@@ -100,7 +101,8 @@ export default async function AdminUsersPage({ searchParams }: { searchParams: P
const username = formData.get('username') as string
const password = formData.get('password') as string
const role = formData.get('role') as string
- const categoryIds = formData.getAll('categories').map(id => parseInt(id as string)).filter(id => !isNaN(id))
+ const divisionIdStr = formData.get('divisionId') as string
+ const divisionId = divisionIdStr ? parseInt(divisionIdStr) : null
const qs = `&edit=${id}&name=${encodeURIComponent(name||'')}&email=${encodeURIComponent(email||'')}&username=${encodeURIComponent(username||'')}&role=${encodeURIComponent(role||'')}`
@@ -108,10 +110,10 @@ export default async function AdminUsersPage({ searchParams }: { searchParams: P
if (!existingUser) return
let hasError = false;
- let errName = '', errEmail = '', errUsername = '', errPassword = '', errCategories = '';
+ let errName = '', errEmail = '', errUsername = '', errPassword = '', errDivision = '';
- if (categoryIds.length === 0) {
- errCategories = 'At least one group/category is required';
+ if (!divisionId && role !== 'super_admin') {
+ errDivision = 'Division is required';
hasError = true;
}
@@ -154,7 +156,7 @@ export default async function AdminUsersPage({ searchParams }: { searchParams: P
}
if (hasError) {
- redirect(`/admin/users?err_name=${encodeURIComponent(errName)}&err_email=${encodeURIComponent(errEmail)}&err_username=${encodeURIComponent(errUsername)}&err_password=${encodeURIComponent(errPassword)}&err_categories=${encodeURIComponent(errCategories)}${qs}`)
+ redirect(`/admin/users?err_name=${encodeURIComponent(errName)}&err_email=${encodeURIComponent(errEmail)}&err_username=${encodeURIComponent(errUsername)}&err_password=${encodeURIComponent(errPassword)}&err_division=${encodeURIComponent(errDivision)}${qs}`)
}
// Only update fields that are provided
@@ -163,7 +165,7 @@ export default async function AdminUsersPage({ searchParams }: { searchParams: P
if (email) updateData.email = email
if (username) updateData.username = username
if (password) updateData.password = password
- updateData.categories = { set: categoryIds.map(id => ({ id })) }
+ updateData.divisionId = divisionId
await prisma.user.update({
where: { id },
@@ -265,16 +267,14 @@ export default async function AdminUsersPage({ searchParams }: { searchParams: P
)}
-
-
- {categories.map(cat => (
-
+
+
- {err_categories && {err_categories} }
+
+ {err_division && {err_division} }
@@ -315,16 +315,14 @@ export default async function AdminUsersPage({ searchParams }: { searchParams: P
-
-
- {categories.map(cat => (
-
+
+
- {err_categories && {err_categories} }
+
+ {err_division && {err_division} }
@@ -358,9 +356,7 @@ export default async function AdminUsersPage({ searchParams }: { searchParams: P
|
- {user.categories.length > 0
- ? user.categories.map(c => c.name).join(', ')
- : -}
+ {user.division ? user.division.name : -}
|
diff --git a/src/app/approvals/page.tsx b/src/app/approvals/page.tsx
index 858a94e..c998352 100644
--- a/src/app/approvals/page.tsx
+++ b/src/app/approvals/page.tsx
@@ -22,12 +22,11 @@ export default async function ApprovalsPage({ searchParams }: { searchParams: Pr
redirect('/')
}
- const approver = await prisma.user.findUnique({ where: { id: session.id }, include: { categories: true } })
+ const approver = await prisma.user.findUnique({ where: { id: session.id } })
let categoryFilter: any = {};
if (session.role !== 'super_admin' && session.role !== 'warehouse' && session.role !== 'finance') {
- const allowedCategoryIds = approver?.categories.map(c => c.id) || [];
- categoryFilter = { item: { categoryId: { in: allowedCategoryIds } } };
+ categoryFilter = { item: { category: { divisionId: approver?.divisionId } } };
}
// Get all requests
@@ -49,16 +48,13 @@ export default async function ApprovalsPage({ searchParams }: { searchParams: Pr
}
}
- const allowedCategoryIdsForFinance = approver?.categories.map(c => c.id) || [];
-
const pendingRequests = allRequests.filter(r => {
const isPending = r.status === 'pending' || r.status === 'approved_1';
if (!isPending) return false;
- // Finance only sees pending requests for their division
- if (session.role === 'finance') {
- return r.item.categoryId && allowedCategoryIdsForFinance.includes(r.item.categoryId);
- }
+ // Finance only sees pending requests for their division (if it's at step 2)
+ // Wait, let them see all pending requests, but they can only approve their own.
+ // Actually, to keep it clean, finance sees all pending requests so they can track it.
return true;
});
@@ -130,11 +126,11 @@ export default async function ApprovalsPage({ searchParams }: { searchParams: Pr
// Cannot approve own request (unless super admin override)
if (req.userId === currentSession.id && currentSession.role !== 'super_admin') return
- // Enforcement for Finance role on bulk or direct calls
- if (currentSession.role === 'finance') {
- const financeUser = await prisma.user.findUnique({ where: { id: currentSession.id }, include: { categories: true } });
- const financeAllowedCategories = financeUser?.categories.map(c => c.id) || [];
- if (!req.item.categoryId || !financeAllowedCategories.includes(req.item.categoryId)) {
+ // Enforcement for Finance and Approver role on bulk or direct calls
+ if (currentSession.role === 'finance' || currentSession.role === 'approver') {
+ const userDiv = await prisma.user.findUnique({ where: { id: currentSession.id } });
+ const itemWithCat = await prisma.item.findUnique({ where: { id: req.itemId }, include: { category: true } });
+ if (itemWithCat?.category?.divisionId !== userDiv?.divisionId) {
return;
}
}
@@ -383,9 +379,19 @@ export default async function ApprovalsPage({ searchParams }: { searchParams: Pr
const firstApproverId = req.approvalLogs.find(l => l.approvalStep === 1)?.approverId
const isOwnRequest = req.userId === session.id
- const isFinance = session.role === 'finance';
- const isItemInFinanceCategories = req.item.categoryId && allowedCategoryIdsForFinance.includes(req.item.categoryId);
- const canApprove = (session.role === 'approver' || session.role === 'warehouse' || (isFinance && isItemInFinanceCategories)) && !isOwnRequest && (!isStep2 || firstApproverId !== session.id)
+ let canApprove = false;
+ if (session.role === 'super_admin') {
+ canApprove = true;
+ } else if (session.role === 'warehouse') {
+ if (req.status === 'pending') canApprove = true; // Warehouse does Step 1
+ } else if (session.role === 'approver' || session.role === 'finance') {
+ if (req.status === 'approved_1' && req.item.category?.divisionId === approver?.divisionId) {
+ canApprove = true; // Division owner does Step 2
+ }
+ // If it's a 1-step process, maybe let division owner approve it too if warehouse hasn't?
+ // We'll stick to the strict flow: Warehouse -> Division
+ }
+ if (isOwnRequest && session.role !== 'super_admin') canApprove = false;
return (
|
@@ -480,7 +486,7 @@ export default async function ApprovalsPage({ searchParams }: { searchParams: Pr
/>
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 08a1d5f..ccb3322 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -21,12 +21,11 @@ export default async function RootLayout({
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 user = session ? await prisma.user.findUnique({ where: { id: session.id } }) : null
const requests = await prisma.request.findMany({
where: {
status: { in: ['pending', 'approved_1'] },
- item: { categoryId: { in: allowedCategoryIds } }
+ item: { category: { divisionId: user?.divisionId } }
},
include: { approvalLogs: true }
})
@@ -35,7 +34,7 @@ export default async function RootLayout({
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
+ const firstApproverId = r.approvalLogs.find((l: any) => l.approvalStep === 1)?.approverId
return firstApproverId !== session.id
}).length
}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 23c0276..4aff6c2 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -7,13 +7,13 @@ import { Category } from '@prisma/client'
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
+ const user = await prisma.user.findUnique({ where: { id: session?.id }, include: { division: true } })
let allowedCategories: Category[] = []
- if (session?.role === 'super_admin') {
+ if (session?.role === 'super_admin' || session?.role === 'warehouse' || session?.role === 'finance') {
allowedCategories = await prisma.category.findMany()
- } else if (user) {
- allowedCategories = user.categories
+ } else if (user?.divisionId) {
+ allowedCategories = await prisma.category.findMany({ where: { divisionId: user.divisionId } })
}
let displayCategories = allowedCategories
@@ -64,7 +64,7 @@ export default async function Dashboard({ searchParams }: { searchParams: Promis
},
{ 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 })
+ groupedData.push({ category: { id: 0, name: 'Uncategorized', createdAt: new Date(), updatedAt: new Date(), divisionId: null }, items: uncategorizedItems as any[], metrics })
}
return (
diff --git a/src/app/request/page.tsx b/src/app/request/page.tsx
index fc9452d..b0cc127 100644
--- a/src/app/request/page.tsx
+++ b/src/app/request/page.tsx
@@ -22,19 +22,9 @@ export default async function RequestPage({ searchParams }: { searchParams: Prom
redirect('/')
}
- const user = await prisma.user.findUnique({ where: { id: session.id }, include: { categories: true } })
- const allowedCategories = session.role === 'super_admin' ? await prisma.category.findMany() : (user?.categories || [])
- const allowedCategoryIds = allowedCategories.map(c => c.id)
-
- let items;
- if (session.role === 'super_admin') {
- items = await prisma.item.findMany({ include: { category: true } })
- } else {
- items = await prisma.item.findMany({
- where: { categoryId: { in: allowedCategoryIds } },
- include: { category: true }
- })
- }
+ const user = await prisma.user.findUnique({ where: { id: session.id } })
+ const items = await prisma.item.findMany({ include: { category: { include: { division: true } } } })
+ const categories = await prisma.category.findMany()
let userRequests = await prisma.request.findMany({
where: { userId: session.id },
@@ -298,7 +288,7 @@ export default async function RequestPage({ searchParams }: { searchParams: Prom
/* Cascading dropdown logic */
#itemSelect option[data-cat] { display: none; }
form:has(#categoryFilter option[value="all"]:checked) #itemSelect option[data-cat] { display: block; }
- ${allowedCategories.map(cat => `
+ ${categories.map(cat => `
form:has(#categoryFilter option[value="${cat.id}"]:checked) #itemSelect option[data-cat="${cat.id}"] { display: block; }
`).join('\n')}
`}
@@ -306,7 +296,7 @@ export default async function RequestPage({ searchParams }: { searchParams: Prom
@@ -330,7 +320,7 @@ export default async function RequestPage({ searchParams }: { searchParams: Prom
@@ -422,7 +412,7 @@ export default async function RequestPage({ searchParams }: { searchParams: Prom
/>
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index 4e7d93b..542c923 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -39,6 +39,7 @@ export default function Navbar({ role, name, pendingCount = 0 }: { role: string,
)}
{role === 'super_admin' && (
<>
+ Divisions
Categories
Users
Settings