
[ad_1]
Build attendance management app

In this article, we will learn how to build a full-stack application using Next.js, Prisma, Postgres, and Fastify. We will create an attendance management demo application that manages the attendance of employees. The flow of the app is simple: An administrative user logs in, creates an attendance sheet for the day, then each employee signs in and out of the attendance sheet.
Next.js is a flexible React framework that gives you the building blocks to build fast web applications. It is often called a full-stack React framework because it makes it possible to have both frontend and backend applications on the same codebase with serverless functions.
Prisma is an open-source, Node.js and TypeScript ORM that greatly simplifies data modeling, migration, and data access for SQL databases. At the time of writing this article, Prisma supports the following database management systems: PostgreSQL, MySQL, MariaDB, SQLite, AWS Aurora, Microsoft SQL Server, Azure SQL, and MongoDB. you may also want to click Here To see a list of all supported database management systems.
Postgres is also known as PostgreSQL and is a free and open-source relational database management system. It is a superset of the SQL language, and has many features that allow developers to securely store and scale complex data workloads.
This tutorial is a practical demonstration tutorial. So, it would be best if you did the following on your computer:
The code for this tutorial is available Here on Github, so feel free to clone it and follow along.
Let’s start by setting up our Next.js application. To get started, run the command below.
npx create-next-app@latest
Wait for the installation to complete, and then run the command below to install our dependencies.
yarn add fastify fastify-nextjs iron-session @prisma/client
yarn add prisma nodemon --dev
Wait for the installation to complete.
By default, Next.js does not use Fastify as its server. To use Fastify to serve our Next.js app, edit the script field in package.json
file with the below code snippet.
"scripts": {
"dev": "nodemon server.js",
"build": "next build",
"start": "next start",
"lint": "next lint"
}
now make one server.js
file. This file is the entry point of our application, and then we add require('fastify-nextjs')
To include a plugin exposing the Next.js API in Fastify to handle the rendering.
open server.js
file, and add the code snippet below:
const fastify = require('fastify')()
async function noOpParser(req, payload) {
return payload;
}
fastify.register(require('fastify-nextjs')).after(() => {
fastify.addContentTypeParser('text/plain', noOpParser);
fastify.addContentTypeParser('application/json', noOpParser);
fastify.next('/*')
fastify.next('/api/*', { method: 'ALL' });
})
fastify.listen(3000, err => {
if (err) throw err
console.log('Server listening on <http://localhost:3000>')
})
In the above code snippet, we use fastify-nextjs
Plugin that exposes the Next.js API in Fastify which handles the rendering for us. Then we parse the incoming requests noOpParser
The function that makes the request body available to our Next.js API route handler and we define two routes for our app: [fastify.next](<http://fastify.next>
command. Then we create our Fastify server and have it listen on port 3000.
Now go ahead and run the app with yarn dev
Command: the app will be running localhost:3000
,
First, run the following command to get the basic Prisma setup:
npx prisma init
The above command a. will create a Prisma directory with schema.prisma
file. This is your main Prisma configuration file that will contain your database schema. In addition, a .env
The file will be added to the root of the project. open .env
file and replace the dummy connection URL with the connection URL for your PostgreSQL database.
change code in prisma/schema.prisma
File with the following:
datasource db {
url = env("DATABASE_URL")
provider="postgresql"
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
email String @unique
name String
password String
role Role @default(EMPLOYEE)
attendance Attendance[]
AttendanceSheet AttendanceSheet[]
}
model AttendanceSheet {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy User? @relation(fields: [userId], references: [id])
userId Int?
}
model Attendance {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
signIn Boolean @default(true)
signOut Boolean
signInTime DateTime @default(now())
signOutTime DateTime
user User? @relation(fields: [userId], references: [id])
userId Int?
}
enum Role {
EMPLOYEE
ADMIN
}
In the above code snippet, we have created a User, Attendance Sheet, and Attendance model, defining the relationships between each model.
Next, create these tables in the database. Run the following command:
npx prisma db push
After running the above command, you should see the output shown in the screenshot below in your terminal:
With Prisma set up, let’s create three utility functions that will be used from time to time within our app.
Open the lib/parseBody.js file and add the following code snippet. This function parses the request body to JSON:
export const parseBody = (body) => {
if (typeof body === "string") return JSON.parse(body)
return body
}
Open the lib/request.js file and add the following code snippet. This function sends a POST request.
export const sessionCookie = () => {
return ({
cookieName: "auth",
password: process.env.SESSION_PASSWORD,
// secure: true should be used in production (HTTPS) but can't be used in development (HTTP)
cookieOptions: {
secure: process.env.NODE_ENV === "production",
},
})
}
Open the /lib/request.js file and add the following code snippet. This function iron session returns an object of session properties for iron-session.
export const sessionCookie = () => {
return ({
cookieName: "auth",
password: process.env.SESSION_PASSWORD,
// secure: true should be used in production (HTTPS) but can't be used in development (HTTP)
cookieOptions: {
secure: process.env.NODE_ENV === "production",
},
})
}
Next, add SESSION_PASSWORD
In the .env file: This must be a string of at least 32 characters.
With our utility functions complete, let’s add some styles to the app. we are using css module for this app so open it styles/Home.modules.css
file and add below code snippet:
.container {
padding: 0 2rem;
}
.man {
min-height: 100vh;
padding: 4rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.login {
width: 450px;
}
.login input {
width: 100%;
height: 50px;
margin: 4px;
}
.login button {
width: 100%;
height: 50px;
margin: 4px;
}
.dashboard {
display: grid;
grid-template-columns: 3fr 9fr;
grid-template-rows: 1fr;
grid-column-gap: 0px;
grid-row-gap: 0px;
height: calc(100vh - 60px);
}
.navbar {
height: 60px;
background-color: black;
}
With our styling done, let’s create a sidebar component to help us navigate the different pages on our app dashboard. Open the Components/SideBar.js file, and paste the code snippet below.
import Link from 'next/link'
import { useRouter } from 'next/router'
import styles from '../styles/SideBar.module.css'const SideBar = () => {
const router = useRouter()
const logout = async () => {
try {
const response = await fetch('/api/logout', {
method: 'GET',
credentials: 'same-origin',
});
if(response.status === 200) router.push('/')
} catch (e) {
alert(e)
}
}
return (
<nav className={styles.sidebar}>
<ul>
<li> <Link href="/dashboard"> Dashboard</Link> </li>
<li> <Link href="/dashboard/attendance"> Attendance </Link> </li>
<li> <Link href="/dashboard/attendance-sheet"> Attendance Sheet </Link> </li>
<li onClick={logout}> Logout </li>
</ul>
</nav>
)
}
export default SideBar
Now open the page/index.js file, remove all the code there and add the following code snippet. The code below sends a POST request to localhost:3000/api/login route with the email and password provided through the form. Once the credential is validated it calls router.push('/dashboard')
Method that redirects the user to localhost:3000/api/dashboard:
import Head from 'next/head'
import { postData } from '../lib/request';
import styles from '../styles/Home.module.css'
import { useState } from 'react';
import { useRouter } from 'next/router'export default function Home({posts}) {
const [data, setData] = useState({email: null, password: null});
const router = useRouter()
const submit = (e) => {
e.preventDefault()
if(data.email && data.password) {
postData('/api/login', data).then(data => {
console.log(data);
if (data.status === "success") router.push('/dashboard')
});
}
}
return (
<div className={styles.container}>
<Head>
<title>Login</title>
<meta name="description" content="Login" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<form className={styles.login}>
<input
type={"text"}
placeholder="Enter Your Email"
onChange={(e) => setData({...data, email: e.target.value})} />
<input
type={"password"}
placeholder="Enter Your Password"
onChange={(e) => setData({...data, password: e.target.value})} />
<button onClick={submit}>Login</button>
</form>
</main>
</div>
)
}
Now open the page/api/login.js file and add the following code snippet. we will use PrismaClient
to make our database queries and withIronSessionApiRoute
There is iron-session function for handling user sessions in RESTful applications.
It handles the login post request to root localhost:3000/api/login, and generates authentication cookies once the user is authenticated.
import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { parseBody } from '../../lib/parseBody';
import { sessionCookie } from '../../lib/session';export default withIronSessionApiRoute(
async function loginRoute(req, res) {
const { email, password } = parseBody(req.body)
const prisma = new PrismaClient()
// By unique identifier
const user = await prisma.user.findUnique({
where: {
email
},})
if(user.password === password) {
// get user from database then:
user.password = undefined
req.session.user = user
await req.session.save();
return res.send({ status: 'success', data: user });
};
res.send({ status: 'error', message: "incorrect email or password" });
},
sessionCookie(),
);
Open the /pages/api/logout file and add the below code snippet. This root handles GET requests to localhost:3000/api/logout which logs out users by destroying session cookies.
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionCookie } from "../../lib/session";export default withIronSessionApiRoute(
function logoutRoute(req, res, session) {
req.session.destroy();
res.send({ status: "success" });
},
sessionCookie()
);
This page provides an interface for users to sign in and sign out of the attendance sheet. Admins can also create an attendance sheet. Open page/dashboard/index.js file and add below code snippet.
import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { useState, useCallback } from "react";
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";
import { postData } from "../../lib/request";export default function Page(props) {
const [attendanceSheet, setState] = useState(JSON.parse(props.attendanceSheet));
const sign = useCallback((action="") => {
const body = {
attendanceSheetId: attendanceSheet[0]?.id,
action
}
postData("/api/sign-attendance", body).then(data => {
if (data.status === "success") {
setState(prevState => {
const newState = [...prevState]
newState[0].attendance[0] = data.data
return newState
})
}
})
}, [attendanceSheet])
const createAttendance = useCallback(() => {
postData("/api/create-attendance").then(data => {
if (data.status === "success") {
alert("New Attendance Sheet Created")
setState([{...data.data, attendance:[]}])
}
})
}, [])
return (
<div>
<Head>
<title>Attendance Management Dashboard</title>
<meta name="description" content="dashboard" />
</Head>
<div className={styles.navbar}></div>
<main className={styles.dashboard}>
<SideBar />
<div className={dashboard.users}>
{
props.isAdmin && <button className={dashboard.create} onClick={createAttendance}>Create Attendance Sheet</button>
}
{ attendanceSheet.length > 0 &&
<table className={dashboard.table}>
<thead>
<tr>
<th>Id</th> <th>Created At</th> <th>Sign In</th> <th>Sign Out</th>
</tr>
</thead>
<tbody>
<tr>
<td>{attendanceSheet[0]?.id}</td>
<td>{attendanceSheet[0]?.createdAt}</td>
{
attendanceSheet[0]?.attendance.length != 0 ?
<>
<td>{attendanceSheet[0]?.attendance[0]?.signInTime}</td>
<td>{
attendanceSheet[0]?.attendance[0]?.signOut ?
attendanceSheet[0]?.attendance[0]?.signOutTime: <button onClick={() => sign("sign-out")}> Sign Out </button> }</td>
</>
:
<>
<td> <button onClick={() => sign()}> Sign In </button> </td>
<td>{""}</td>
</>
}
</tr>
</tbody>
</table>
}
</div>
</main>
</div>
)
}
we use getServerSideProps
to generate page data, and withIronSessionSsr
There is an iron-op function for working with server-side rendered pages. In the following code snippet, we query for the last row of the attendance sheet table with a row from the attendance table, where userId
User is equal to the userid stored on the session. We also check if the user is an administrator.
export const getServerSideProps = withIronSessionSsr( async ({req}) => {const user = req.session.user
const prisma = new PrismaClient()
const attendanceSheet = await prisma.attendanceSheet.findMany({
take: 1,
orderBy: {
id: 'desc',
},
include: {
attendance: {
where: {
userId: user.id
},
}
}
})
return {
props: {
attendanceSheet: JSON.stringify(attendanceSheet),
isAdmin: user.role === "ADMIN"
}
}
}, sessionCookie())
Open page/api/create-attendance.js file and add below code snippet.
import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionCookie } from '../../lib/session';export default withIronSessionApiRoute( async function handler(req, res) {
const prisma = new PrismaClient()
const user = req.session.user
const attendanceSheet = await prisma.attendanceSheet.create({
data: {
userId: user.id,
},
})
res.json({status: "success", data: attendanceSheet});
}, sessionCookie())
This route handles our API post request to localhost:3000/api/sign-attendance. while route accepts POST request attendanceSheetId
And action
used to sign in and out attendanceSheet
,
Open the /pages/api/sign-attendance.js file and add the below code snippet.
import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { parseBody } from '../../lib/parseBody';
import { sessionCookie } from '../../lib/session';export default withIronSessionApiRoute( async function handler(req, res) {
const prisma = new PrismaClient()
const {attendanceSheetId, action} = parseBody(req.body)
const user = req.session.user
const attendance = await prisma.attendance.findMany({
where: {
userId: user.id,
attendanceSheetId: attendanceSheetId
}
})
//check if atendance have been created
if (attendance.length === 0) {
const attendance = await prisma.attendance.create({
data: {
userId: user.id,
attendanceSheetId: attendanceSheetId,
signIn: true,
signOut: false,
signOutTime: new Date()
},
})
return res.json({status: "success", data: attendance});
} else if (action === "sign-out") {
await prisma.attendance.updateMany({
where: {
userId: user.id,
attendanceSheetId: attendanceSheetId
},
data: {
signOut: true,
signOutTime: new Date()
},
})
return res.json({status: "success", data: { ...attendance[0], signOut: true, signOutTime: new Date()}});
}
res.json({status: "success", data: attendance});
}, sessionCookie())
This server-side rendered page shows all the attendance sheets for the logged-in user. Open the /pages/dashboard/attendance.js file and add the below code snippet.
import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";export default function Page(props) {
const data = JSON.parse(props.attendanceSheet)
return (
<div>
<Head>
<title>Attendance Management Dashboard</title>
<meta name="description" content="dashboard" />
</Head>
<div className={styles.navbar}></div>
<main className={styles.dashboard}>
<SideBar />
<div className={dashboard.users}>
<table className={dashboard.table}>
<thead>
<tr>
<th> Attendance Id</th> <th>Date</th>
<th>Sign In Time</th> <th>Sign Out Time</th>
</tr>
</thead>
<tbody>
{
data.map(data => {
const {id, createdAt, attendance } = data
return (
<tr key={id}>
<td>{id}</td> <td>{createdAt}</td>
{ attendance.length === 0 ?
(
<>
<td>You did not Sign In</td>
<td>You did not Sign Out</td>
</>
)
:
(
<>
<td>{attendance[0]?.signInTime}</td>
<td>{attendance[0]?.signOut ? attendance[0]?.signOutTime : "You did not Sign Out"}</td>
</>
)
}
</tr>
)
})
}
</tbody>
</table>
</div>
</main>
</div>
)
}
In the below code snippet, we query for all rows attendanceSheet
also get table and attendance where userId
User is equal to the userid stored in the session.
export const getServerSideProps = withIronSessionSsr( async ({req}) => {const user = req.session.user
const prisma = new PrismaClient()
const attendanceSheet = await prisma.attendanceSheet.findMany({
orderBy: {
id: 'desc',
},
include: {
attendance: {
where: {
userId: user.id
},
}
}
})
return {
props: {
attendanceSheet: JSON.stringify(attendanceSheet),
}
}
}, sessionCookie())
This server-side rendered page shows all attendance sheets and the employees who signed in to that attendance sheet. Open the /pages/dashboard/attendance.js file and add the below code snippet.
import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";export default function Page(props) {
const data = JSON.parse(props.attendanceSheet)
return (
<div>
<Head>
<title>Attendance Management Dashboard</title>
<meta name="description" content="dashboard" />
</Head>
<div className={styles.navbar}></div>
<main className={styles.dashboard}>
<SideBar />
<div className={dashboard.users}>
{
data?.map(data => {
const {id, createdAt, attendance } = data
return (
<>
<table key={data.id} className={dashboard.table}>
<thead>
<tr>
<th> Attendance Id</th> <th>Date</th>
<th> Name </th> <th> Email </th> <th> Role </th>
<th>Sign In Time</th> <th>Sign Out Time</th>
</tr>
</thead>
<tbody>
{
(attendance.length === 0) &&
(
<>
<tr><td> {id} </td> <td>{createdAt}</td> <td colSpan={5}> No User signed this sheet</td></tr>
</>
)
}
{
attendance.map(data => {
const {name, email, role} = data.user
return (
<tr key={id}>
<td>{id}</td> <td>{createdAt}</td>
<td>{name}</td> <td>{email}</td>
<td>{role}</td>
<td>{data.signInTime}</td>
<td>{data.signOut ? attendance[0]?.signOutTime: "User did not Sign Out"}</td>
</tr>
)
})
}
</tbody>
</table>
</>
)
})
}
</div>
</main>
</div>
)
}
In the below code snippet, we query for all rows attendanceSheet
Also get attendance by selecting table and name, email and role.
export const getServerSideProps = withIronSessionSsr(async () => {const prisma = new PrismaClient()
const attendanceSheet = await prisma.attendanceSheet.findMany({
orderBy: {
id: 'desc',
},
include: {
attendance: {
include: {
user: {
select: {
name: true,
email: true,
role: true
}
}
}
},
},
})
return {
props: {
attendanceSheet: JSON.stringify(attendanceSheet),
}
}
}, sessionCookie())
First, we need to add users to our database. We’re going to do it with Prisma Studio. To start Prisma Studio, run the command below:
npx prisma studio
The Prisma index page looks like this:
To create a database user with an administrator role and multiple users with an employee role, visit this page:
Click Add Record, then fill in the required fields: Password, Name, Email, and Role. Once you are done, click on the green Save 1 Changes button. Note that for the sake of simplicity, we didn’t have a password hash.
start the server with yarn dev
, This starts the server and starts the app [localhost:3000],
Log in with a user who has an administrator role as only administrative users can create attendance sheets. After the login is successful, the app will redirect you to your dashboard.
To generate the Attendance Sheet, click on the Create Attendance Sheet button, then wait for the request to be over and the Attendance Sheet will appear. The user dashboard is shown below.
The attendance sheet is shown below, click on the Sign In button to sign in. After the sign-in is successful, the sign-in time will be displayed and the sign out button will appear. Click the Sign Out button to sign out, and repeat this process several times with different users.
Next, click the Attendance link in the sidebar to view users’ attendance. The result should match as shown below:
Next, click the Attendance Sheet link on the sidebar to view the attendance of all users. The results are shown below:
In this article, you learned how to use a custom Fastify server with Next.js. You also learned about Prisma and Prisma Studio. I have told you how to connect Prisma to Postgres database and how to create, read and update database using Prisma Client and Prisma Studio.
You also learned how to authenticate users using iron-op. In this tutorial, we have built a full-stack app that manages employee attendance using Next.js, Prisma, Postgres, and Fastify. For more and be sure to stay tuned till next time.
[ad_2]
Source link
#Build #Fullstack #App #Next.js #Prisma #Postgres #Fastify #Archetype #July