Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

471 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Klapi

A web-based equipment loan management system for organizations. Browse inventory, request loans, and manage returns with automated email notifications.

Features

  • Browse equipment catalog with search and filtering
  • Request loans with flexible date ranges
  • Automated email notifications for loan reminders and updates
  • Admin dashboard for managing inventory, locations, and loans
  • Multi-user support with role-based permissions (Admin, User, Kiosk)
  • Support for normal and temporary items
  • Organized inventory with categories, locations, and boxes

Workflows

For Users

  1. Browse available equipment in the catalog
  2. Request a loan by selecting dates and items
  3. Receive an email confirmation right away — requests are accepted automatically, there is no approval queue to wait for
  4. Get automated reminders before pickup, and when a loan runs late
  5. Return items and view loan history

For Admins

  1. Manage equipment catalog (add, edit, remove items)
  2. Organize items by categories, locations, and boxes
  3. Track all active and past loans; reject or cancel ones that shouldn't run (rejecting is after the fact — see the note on automatic acceptance above)
  4. Handle returns, including items dropped into a return box
  5. Manage user accounts and permissions

For Kiosk Mode

  • Self-service stations for quick item checkout and returns
  • Simplified interface for public access points

Tech Stack

UI & theming

  • All UI primitives live in components/ui/ — they're source files you own and edit freely (shadcn pattern, not an installed library).
  • Design tokens are CSS variables in styles/globals.css. Colors map to HSL vars (--primary, --background, --card, --destructive, --success, --warning, etc.) with .dark overrides.
  • There is no tailwind.config.ts — Tailwind 4 is configured CSS-first. styles/globals.css does @import 'tailwindcss' and exposes the tokens through an @theme block, which is what makes bg-primary, text-muted-foreground etc. work. PostCSS wires it up via @tailwindcss/postcss in postcss.config.js. Dark mode is class-based — toggled by next-themes via the class attribute on <html>.
  • Toasts use sonner. Import toast from sonner and call toast.success(...), toast.error(...), toast.warning(...).
  • Creatable selects use the CreatableSelect wrapper in components/ui/creatable-select.tsx — it styles react-select's creatable via the classNames API so it respects dark mode and tokens without runtime theme juggling.

Development

  1. Install dependencies:
pnpm install
  1. Set up environment variables:
cp .env.example .env

The defaults in .env.example match docker-compose.yml, so the database works out of the box; the AWS and Google values are only needed for real email, photo uploads, and Google sign-in.

  1. Start local database:
docker-compose up -d
  1. Run migrations and seed data:
pnpm prisma migrate dev
pnpm prisma db seed
  1. Start development server (also boots the local SES mock automatically):
pnpm dev

Visit http://localhost:3000.

Useful scripts

  • pnpm dev — Next dev server + local SES mock
  • pnpm buildprisma migrate deploy + next build
  • pnpm start — Production server
  • pnpm type-checktsc --noEmit
  • pnpm lint — ESLint (Next config)
  • pnpm test — Vitest against a disposable Postgres (docker-compose)
  • pnpm test:ci — Vitest without docker (expects DATABASE_URL already set)

Local Email Testing

The dev script starts aws-ses-v2-local in parallel. All emails are captured instead of being sent.

Open the email viewer at http://localhost:8005 to see sent emails.

To review the templates without triggering the flows that send them:

pnpm tsx scripts/preview-emails.ts

It renders every template with sample data — no app or database needed — to /tmp/klapi-emails/, with an index.html linking them all.

Database

Schema is defined in prisma/schema.prisma. After schema changes:

pnpm prisma migrate dev --name description_of_change

Generate test data:

pnpm prisma db seed

Entity-relationship diagram

The diagram below is generated from prisma/schema.prisma by prisma-erd-generator. Refresh it after a schema change with:

pnpm erd

That regenerates the mermaid source into .erd.md (gitignored) and splices it between the markers below. Markdown output is pure text generation, so it costs ~25 ms — the .pdf/.svg/.png output modes are the expensive ones, because they rasterize through a headless Chrome that puppeteer has to download first. Don't switch the generator's output to one of those.

erDiagram

        ItemHistoryAction {
            CREATED CREATED
UPDATED UPDATED
ARCHIVED ARCHIVED
RESTORED RESTORED
PROMOTED PROMOTED
        }
    


        AnnouncementKind {
            KORJATTAVAA KORJATTAVAA
TIEDOKSI TIEDOKSI
        }
    


        LoanHistoryAction {
            CREATED CREATED
UPDATED UPDATED
APPROVED APPROVED
REJECTED REJECTED
CANCELLED CANCELLED
STARTED STARTED
RETURNED_TO_BOX RETURNED_TO_BOX
PROCESSED_FROM_BOX PROCESSED_FROM_BOX
        }
    


        Group {
            ADMIN ADMIN
USER USER
KIOSK KIOSK
        }
    


        ItemType {
            normal normal
temporary temporary
        }
    


        ReportStatus {
            OPEN OPEN
IN_PROGRESS IN_PROGRESS
RESOLVED RESOLVED
        }
    


        ReportCreated {
            BEFORE_LOAN BEFORE_LOAN
AFTER_LOAN AFTER_LOAN
        }
    


        LoanStatus {
            ACCEPTED ACCEPTED
REJECTED REJECTED
CANCELLED CANCELLED
INUSE INUSE
IN_BOX IN_BOX
PARTIALLY_RETURNED PARTIALLY_RETURNED
RETURNED RETURNED
        }
    


        ReservationStatus {
            ACCEPTED ACCEPTED
REJECTED REJECTED
INUSE INUSE
IN_BOX IN_BOX
RETURNED RETURNED
        }
    


        EmailType {
            EXPIRING_LOAN_REMINDER EXPIRING_LOAN_REMINDER
PICKUP_REMINDER PICKUP_REMINDER
PICKUP_OVERDUE_REMINDER PICKUP_OVERDUE_REMINDER
OVERDUE_USER_REMINDER OVERDUE_USER_REMINDER
OVERDUE_ADMIN_NOTIFICATION OVERDUE_ADMIN_NOTIFICATION
OLD_BOX_ADMIN_NOTIFICATION OLD_BOX_ADMIN_NOTIFICATION
        }
    
  "Account" {
    String id "🗝️"
    String type 
    String provider 
    String providerAccountId 
    String refresh_token "❓"
    String access_token "❓"
    Int expires_at "❓"
    String token_type "❓"
    String scope "❓"
    String id_token "❓"
    String session_state "❓"
    }
  

  "Session" {
    String id "🗝️"
    String sessionToken 
    DateTime expires 
    }
  

  "User" {
    String id "🗝️"
    String name "❓"
    String email "❓"
    DateTime emailVerified "❓"
    String image "❓"
    DateTime deletedAt "❓"
    Group group 
    String password "❓"
    DateTime passwordExpiresAt "❓"
    String kioskPasswordEnc "❓"
    String kioskElevatePin "❓"
    String username "❓"
    Boolean emailNewLoanNotification 
    Boolean emailWeeklyReminder 
    Boolean emailExpiringReminder 
    Boolean emailOldBoxNotification 
    Boolean emailOverdueNotification 
    }
  

  "Item" {
    String id "🗝️"
    String name 
    String description "❓"
    Int amount 
    ItemType type 
    DateTime deletedAt "❓"
    }
  

  "Template" {
    String id "🗝️"
    String name 
    String description "❓"
    DateTime createdAt 
    DateTime updatedAt 
    }
  

  "TemplateItem" {
    String id "🗝️"
    Int amount 
    }
  

  "ItemHistory" {
    String id "🗝️"
    ItemHistoryAction action 
    Json details "❓"
    DateTime createdAt 
    }
  

  "Announcement" {
    String id "🗝️"
    String message 
    AnnouncementKind kind 
    DateTime createdAt 
    DateTime expiresAt "❓"
    }
  

  "Location" {
    String name 
    String description "❓"
    String id "🗝️"
    }
  

  "Box" {
    String id "🗝️"
    String name 
    String description "❓"
    DateTime createdAt 
    DateTime updatedAt 
    }
  

  "Category" {
    String id "🗝️"
    String name 
    String description "❓"
    }
  

  "Reservation" {
    String id "🗝️"
    Int amount 
    ReservationStatus status 
    }
  

  "Loan" {
    String id "🗝️"
    LoanStatus status 
    DateTime startTime 
    DateTime endTime 
    String description "❓"
    String loaner "❓"
    }
  

  "LoanHistory" {
    String id "🗝️"
    LoanHistoryAction action 
    Json details "❓"
    DateTime createdAt 
    }
  

  "Report" {
    String id "🗝️"
    String content 
    ReportStatus status 
    DateTime createdAt 
    ReportCreated created 
    }
  

  "ReportAffectedItem" {
    String id "🗝️"
    Int amount 
    }
  

  "EmailLog" {
    String id "🗝️"
    EmailType emailType 
    DateTime sentAt 
    }
  
    "Account" }o--|| "User" : "user"
    "Session" }o--|| "User" : "user"
    "User" |o--|| "Group" : "enum:group"
    "Item" |o--|| "ItemType" : "enum:type"
    "Item" }o--|o "Location" : "location"
    "Item" o{--}o "Category" : ""
    "TemplateItem" }o--|| "Template" : "template"
    "TemplateItem" }o--|| "Item" : "item"
    "ItemHistory" |o--|| "ItemHistoryAction" : "enum:action"
    "ItemHistory" }o--|| "Item" : "item"
    "ItemHistory" }o--|o "User" : "actedBy"
    "Announcement" |o--|| "AnnouncementKind" : "enum:kind"
    "Announcement" }o--|o "Item" : "item"
    "Announcement" }o--|o "Report" : "report"
    "Reservation" |o--|| "ReservationStatus" : "enum:status"
    "Reservation" }o--|| "Item" : "item"
    "Reservation" }o--|| "Loan" : "loan"
    "Loan" |o--|| "LoanStatus" : "enum:status"
    "Loan" }o--|o "Box" : "box"
    "Loan" }o--|| "User" : "user"
    "LoanHistory" |o--|| "LoanHistoryAction" : "enum:action"
    "LoanHistory" }o--|| "Loan" : "loan"
    "LoanHistory" }o--|o "User" : "actedBy"
    "Report" |o--|| "ReportStatus" : "enum:status"
    "Report" |o--|| "ReportCreated" : "enum:created"
    "Report" }o--|| "Loan" : "loan"
    "ReportAffectedItem" }o--|| "Report" : "report"
    "ReportAffectedItem" }o--|| "Item" : "item"
    "EmailLog" |o--|| "EmailType" : "enum:emailType"
    "EmailLog" }o--|| "Loan" : "loan"
    "EmailLog" }o--|| "User" : "user"
Loading

Authentication

Supports Google OAuth and username/password authentication via NextAuth.js. Configure providers in the NextAuth API route.

User Roles:

  • Admin: Full access to manage catalog, users, and loans
  • User: Browse catalog, request loans, view own history
  • Kiosk: Simplified interface for self-service stations

Admins can elevate a kiosk session to ADMIN temporarily via a 4-digit PIN (set in /admin). The elevated session auto-expires after 30 minutes.

Hosting

Production Deployment

Klapi is deployed automatically when a new commit lands on main.

Environment Variables

See .env.example for the full list with comments. The ones a deployment cannot run without:

  • DATABASE_URL: PostgreSQL connection string
  • NEXTAUTH_SECRET: Random secret for NextAuth (generate with openssl rand -base64 32)
  • NEXTAUTH_URL: Public URL of your deployment
  • GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET: Google OAuth credentials
  • KLAPI_AWS_REGION, KLAPI_AWS_ACCESS_KEY_ID, KLAPI_AWS_SECRET_ACCESS_KEY: AWS credentials for SES and S3 (prefixed because Vercel reserves AWS_*)
  • AWS_SES_FROM_EMAIL: Sender address for all notification email
  • AWS_BUCKET_NAME, NEXT_PUBLIC_AWS_ITEM_PHOTOS_URL: S3 bucket for item photos, and its public URL
  • CRON_SECRET: Shared secret the /api/cron/* routes require as Authorization: Bearer …. Without it the nightly reminder/overdue/auto-start jobs return 401 and silently stop working.

Project layout

app/                App Router — pages (server components by default) and
app/api/            route handlers (`route.ts`), including the cron jobs
components/         App-specific React components
components/ui/      shadcn/ui primitives (owned source, edit freely)
contexts/           React contexts (cart, dates)
hooks/              Custom hooks
lib/                `cn()` className merger and the NextAuth config
styles/globals.css  Tailwind import + `@theme` design tokens (light/dark)
types/              Shared types and the next-auth session augmentation
utils/              Server and shared helpers (Prisma client, loan helpers, etc.)
prisma/             Schema, migrations, seed
scripts/            Operator scripts run with `tsx` (kiosk user, email previews)
__tests__/          Vitest suites — mostly API integration tests

Releases

Packages

Used by

Contributors

Languages