first commit

This commit is contained in:
SatriaHilmi 2025-07-07 14:42:04 +07:00
commit f1a3739492
152 changed files with 12257 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

9
BE/.env.example Normal file
View File

@ -0,0 +1,9 @@
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="" # isi dengan database url contohnya file:./dev.db
PORT=3000 # isi dengan port yang diinginkan
SECRET_KEY="isi dengan secret key bebas" # isi dengan secret key bebas

25
BE/.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
.env
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

12
BE/esbuild.config.js Normal file
View File

@ -0,0 +1,12 @@
// esbuild.config.js
const esbuild = require('esbuild');
esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
// keepNames: true,
platform: 'node',
outfile: 'dist/index.js',
external:['express-list-endpoints', 'express'],
minify: true
}).catch(() => process.exit(1));

2413
BE/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
BE/package.json Normal file
View File

@ -0,0 +1,38 @@
{
"name": "sistem-pendukung-keputusan2",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "ts-node-dev --respawn --transpile-only ./src/index.ts",
"build": "node esbuild.config.js",
"start": "node dist/index.js",
"seedAdmin": "ts-node-dev ./src/utils/seedAdmin.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@prisma/client": "^6.2.1",
"bcryptjs": "^3.0.2",
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express-list-endpoints": "^7.1.1",
"imagekit": "^6.0.0",
"jsonwebtoken": "^9.0.2",
"ts-node": "^10.9.2"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"@types/jsonwebtoken": "^9.0.7",
"@types/node": "^22.10.7",
"esbuild": "^0.25.0",
"prisma": "^6.2.1",
"ts-node-dev": "^2.0.0",
"typescript": "^5.7.3"
}
}

BIN
BE/prisma/dev.db Normal file

Binary file not shown.

View File

@ -0,0 +1,26 @@
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL PRIMARY KEY,
"email" TEXT NOT NULL,
"username" TEXT NOT NULL,
"name" TEXT,
"password" TEXT NOT NULL,
"role" TEXT NOT NULL DEFAULT 'USER'
);
-- CreateTable
CREATE TABLE "Kriteria" (
"id" TEXT NOT NULL PRIMARY KEY,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"weight" INTEGER NOT NULL,
"criteria" TEXT NOT NULL,
"createAt" DATETIME,
"updateAt" DATETIME
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");

View File

@ -0,0 +1,11 @@
-- CreateTable
CREATE TABLE "subKriteria" (
"id" TEXT NOT NULL PRIMARY KEY,
"altenatif" TEXT NOT NULL,
"codeId" TEXT NOT NULL,
"C1" INTEGER NOT NULL,
"C2" INTEGER NOT NULL,
"C3" INTEGER NOT NULL,
"C4" INTEGER NOT NULL,
CONSTRAINT "subKriteria_codeId_fkey" FOREIGN KEY ("codeId") REFERENCES "Kriteria" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);

View File

@ -0,0 +1,25 @@
/*
Warnings:
- You are about to drop the column `altenatif` on the `subKriteria` table. All the data in the column will be lost.
- Added the required column `alternatif` to the `subKriteria` table without a default value. This is not possible if the table is not empty.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_subKriteria" (
"id" TEXT NOT NULL PRIMARY KEY,
"alternatif" TEXT NOT NULL,
"codeId" TEXT NOT NULL,
"C1" INTEGER NOT NULL,
"C2" INTEGER NOT NULL,
"C3" INTEGER NOT NULL,
"C4" INTEGER NOT NULL,
CONSTRAINT "subKriteria_codeId_fkey" FOREIGN KEY ("codeId") REFERENCES "Kriteria" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_subKriteria" ("C1", "C2", "C3", "C4", "codeId", "id") SELECT "C1", "C2", "C3", "C4", "codeId", "id" FROM "subKriteria";
DROP TABLE "subKriteria";
ALTER TABLE "new_subKriteria" RENAME TO "subKriteria";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@ -0,0 +1,27 @@
/*
Warnings:
- You are about to drop the column `C1` on the `subKriteria` table. All the data in the column will be lost.
- You are about to drop the column `C2` on the `subKriteria` table. All the data in the column will be lost.
- You are about to drop the column `C3` on the `subKriteria` table. All the data in the column will be lost.
- You are about to drop the column `C4` on the `subKriteria` table. All the data in the column will be lost.
- You are about to drop the column `codeId` on the `subKriteria` table. All the data in the column will be lost.
- Added the required column `code` to the `subKriteria` table without a default value. This is not possible if the table is not empty.
- Added the required column `nilai` to the `subKriteria` table without a default value. This is not possible if the table is not empty.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_subKriteria" (
"id" TEXT NOT NULL PRIMARY KEY,
"alternatif" TEXT NOT NULL,
"code" TEXT NOT NULL,
"nilai" TEXT NOT NULL,
CONSTRAINT "subKriteria_code_fkey" FOREIGN KEY ("code") REFERENCES "Kriteria" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_subKriteria" ("alternatif", "id") SELECT "alternatif", "id" FROM "subKriteria";
DROP TABLE "subKriteria";
ALTER TABLE "new_subKriteria" RENAME TO "subKriteria";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@ -0,0 +1,26 @@
/*
Warnings:
- You are about to drop the column `code` on the `subKriteria` table. All the data in the column will be lost.
- A unique constraint covering the columns `[code]` on the table `Kriteria` will be added. If there are existing duplicate values, this will fail.
- Added the required column `codeId` to the `subKriteria` table without a default value. This is not possible if the table is not empty.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_subKriteria" (
"id" TEXT NOT NULL PRIMARY KEY,
"alternatif" TEXT NOT NULL,
"codeId" TEXT NOT NULL,
"nilai" TEXT NOT NULL,
CONSTRAINT "subKriteria_codeId_fkey" FOREIGN KEY ("codeId") REFERENCES "Kriteria" ("code") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_subKriteria" ("alternatif", "id", "nilai") SELECT "alternatif", "id", "nilai" FROM "subKriteria";
DROP TABLE "subKriteria";
ALTER TABLE "new_subKriteria" RENAME TO "subKriteria";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;
-- CreateIndex
CREATE UNIQUE INDEX "Kriteria_code_key" ON "Kriteria"("code");

View File

@ -0,0 +1,21 @@
/*
Warnings:
- You are about to alter the column `nilai` on the `subKriteria` table. The data in that column could be lost. The data in that column will be cast from `String` to `Int`.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_subKriteria" (
"id" TEXT NOT NULL PRIMARY KEY,
"alternatif" TEXT NOT NULL,
"codeId" TEXT NOT NULL,
"nilai" INTEGER NOT NULL,
CONSTRAINT "subKriteria_codeId_fkey" FOREIGN KEY ("codeId") REFERENCES "Kriteria" ("code") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_subKriteria" ("alternatif", "codeId", "id", "nilai") SELECT "alternatif", "codeId", "id", "nilai" FROM "subKriteria";
DROP TABLE "subKriteria";
ALTER TABLE "new_subKriteria" RENAME TO "subKriteria";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@ -0,0 +1,15 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_subKriteria" (
"id" TEXT NOT NULL PRIMARY KEY,
"alternatif" TEXT NOT NULL,
"codeId" TEXT NOT NULL,
"nilai" INTEGER NOT NULL,
CONSTRAINT "subKriteria_codeId_fkey" FOREIGN KEY ("codeId") REFERENCES "Kriteria" ("code") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_subKriteria" ("alternatif", "codeId", "id", "nilai") SELECT "alternatif", "codeId", "id", "nilai" FROM "subKriteria";
DROP TABLE "subKriteria";
ALTER TABLE "new_subKriteria" RENAME TO "subKriteria";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "photo" TEXT;

View File

@ -0,0 +1,40 @@
/*
Warnings:
- Added the required column `alatMusikId` to the `subKriteria` table without a default value. This is not possible if the table is not empty.
*/
-- CreateTable
CREATE TABLE "JenisAlatMusik" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL
);
-- CreateTable
CREATE TABLE "AlatMusik" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"jenisId" TEXT NOT NULL,
CONSTRAINT "AlatMusik_jenisId_fkey" FOREIGN KEY ("jenisId") REFERENCES "JenisAlatMusik" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_subKriteria" (
"id" TEXT NOT NULL PRIMARY KEY,
"alternatif" TEXT NOT NULL,
"codeId" TEXT NOT NULL,
"nilai" INTEGER NOT NULL,
"alatMusikId" TEXT NOT NULL,
CONSTRAINT "subKriteria_codeId_fkey" FOREIGN KEY ("codeId") REFERENCES "Kriteria" ("code") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "subKriteria_alatMusikId_fkey" FOREIGN KEY ("alatMusikId") REFERENCES "AlatMusik" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_subKriteria" ("alternatif", "codeId", "id", "nilai") SELECT "alternatif", "codeId", "id", "nilai" FROM "subKriteria";
DROP TABLE "subKriteria";
ALTER TABLE "new_subKriteria" RENAME TO "subKriteria";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;
-- CreateIndex
CREATE UNIQUE INDEX "JenisAlatMusik_name_key" ON "JenisAlatMusik"("name");

View File

@ -0,0 +1,43 @@
/*
Warnings:
- You are about to drop the column `name` on the `JenisAlatMusik` table. All the data in the column will be lost.
- You are about to drop the column `alternatif` on the `subKriteria` table. All the data in the column will be lost.
- You are about to drop the column `codeId` on the `subKriteria` table. All the data in the column will be lost.
- Added the required column `codeId` to the `AlatMusik` table without a default value. This is not possible if the table is not empty.
- Added the required column `nama` to the `JenisAlatMusik` table without a default value. This is not possible if the table is not empty.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_AlatMusik" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"codeId" TEXT NOT NULL,
"jenisId" TEXT NOT NULL,
CONSTRAINT "AlatMusik_codeId_fkey" FOREIGN KEY ("codeId") REFERENCES "Kriteria" ("code") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "AlatMusik_jenisId_fkey" FOREIGN KEY ("jenisId") REFERENCES "JenisAlatMusik" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_AlatMusik" ("id", "jenisId", "name") SELECT "id", "jenisId", "name" FROM "AlatMusik";
DROP TABLE "AlatMusik";
ALTER TABLE "new_AlatMusik" RENAME TO "AlatMusik";
CREATE TABLE "new_JenisAlatMusik" (
"id" TEXT NOT NULL PRIMARY KEY,
"nama" TEXT NOT NULL
);
INSERT INTO "new_JenisAlatMusik" ("id") SELECT "id" FROM "JenisAlatMusik";
DROP TABLE "JenisAlatMusik";
ALTER TABLE "new_JenisAlatMusik" RENAME TO "JenisAlatMusik";
CREATE UNIQUE INDEX "JenisAlatMusik_nama_key" ON "JenisAlatMusik"("nama");
CREATE TABLE "new_subKriteria" (
"id" TEXT NOT NULL PRIMARY KEY,
"nilai" INTEGER NOT NULL,
"alatMusikId" TEXT NOT NULL,
CONSTRAINT "subKriteria_alatMusikId_fkey" FOREIGN KEY ("alatMusikId") REFERENCES "AlatMusik" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_subKriteria" ("alatMusikId", "id", "nilai") SELECT "alatMusikId", "id", "nilai" FROM "subKriteria";
DROP TABLE "subKriteria";
ALTER TABLE "new_subKriteria" RENAME TO "subKriteria";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@ -0,0 +1,32 @@
/*
Warnings:
- You are about to drop the `AlatMusik` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the column `alatMusikId` on the `subKriteria` table. All the data in the column will be lost.
- Added the required column `alternatif` to the `subKriteria` table without a default value. This is not possible if the table is not empty.
- Added the required column `codeId` to the `subKriteria` table without a default value. This is not possible if the table is not empty.
- Added the required column `jenisId` to the `subKriteria` table without a default value. This is not possible if the table is not empty.
*/
-- DropTable
PRAGMA foreign_keys=off;
DROP TABLE "AlatMusik";
PRAGMA foreign_keys=on;
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_subKriteria" (
"id" TEXT NOT NULL PRIMARY KEY,
"alternatif" TEXT NOT NULL,
"codeId" TEXT NOT NULL,
"jenisId" TEXT NOT NULL,
"nilai" INTEGER NOT NULL,
CONSTRAINT "subKriteria_codeId_fkey" FOREIGN KEY ("codeId") REFERENCES "Kriteria" ("code") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "subKriteria_jenisId_fkey" FOREIGN KEY ("jenisId") REFERENCES "JenisAlatMusik" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_subKriteria" ("id", "nilai") SELECT "id", "nilai" FROM "subKriteria";
DROP TABLE "subKriteria";
ALTER TABLE "new_subKriteria" RENAME TO "subKriteria";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@ -0,0 +1,24 @@
/*
Warnings:
- You are about to drop the column `jenisId` on the `subKriteria` table. All the data in the column will be lost.
- Added the required column `namaId` to the `subKriteria` table without a default value. This is not possible if the table is not empty.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_subKriteria" (
"id" TEXT NOT NULL PRIMARY KEY,
"alternatif" TEXT NOT NULL,
"codeId" TEXT NOT NULL,
"namaId" TEXT NOT NULL,
"nilai" INTEGER NOT NULL,
CONSTRAINT "subKriteria_codeId_fkey" FOREIGN KEY ("codeId") REFERENCES "Kriteria" ("code") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "subKriteria_namaId_fkey" FOREIGN KEY ("namaId") REFERENCES "JenisAlatMusik" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_subKriteria" ("alternatif", "codeId", "id", "nilai") SELECT "alternatif", "codeId", "id", "nilai" FROM "subKriteria";
DROP TABLE "subKriteria";
ALTER TABLE "new_subKriteria" RENAME TO "subKriteria";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@ -0,0 +1,17 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_subKriteria" (
"id" TEXT NOT NULL PRIMARY KEY,
"alternatif" TEXT NOT NULL,
"codeId" TEXT NOT NULL,
"namaId" TEXT NOT NULL,
"nilai" INTEGER NOT NULL,
CONSTRAINT "subKriteria_codeId_fkey" FOREIGN KEY ("codeId") REFERENCES "Kriteria" ("code") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "subKriteria_namaId_fkey" FOREIGN KEY ("namaId") REFERENCES "JenisAlatMusik" ("nama") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_subKriteria" ("alternatif", "codeId", "id", "namaId", "nilai") SELECT "alternatif", "codeId", "id", "namaId", "nilai" FROM "subKriteria";
DROP TABLE "subKriteria";
ALTER TABLE "new_subKriteria" RENAME TO "subKriteria";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "sqlite"

58
BE/prisma/schema.prisma Normal file
View File

@ -0,0 +1,58 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
username String @unique
name String?
password String
role Role @default(USER)
photo String?
}
enum Role {
USER
ADMIN
}
model Kriteria {
id String @id @default(cuid())
code String @unique
name String
weight Int
criteria Criteria
subKriteria subKriteria[]
createAt DateTime?
updateAt DateTime?
}
enum Criteria {
COST
BENEFIT
}
model JenisAlatMusik {
id String @id @default(cuid())
nama String @unique
subKriteria subKriteria[]
}
model subKriteria {
id String @id @default(cuid())
alternatif String
codeId String
namaId String
nilai Int
kriteria Kriteria @relation(fields: [codeId], references: [code], onDelete: Cascade)
jenis JenisAlatMusik @relation(fields: [namaId], references: [nama], onDelete: Cascade)
}

33
BE/src/index.ts Normal file
View File

@ -0,0 +1,33 @@
import express from 'express'
import { printRoutes } from './utils/colorConsole'
import { config } from 'dotenv'
import corsSetup from 'cors'
config()
const app = express()
app.use(express.json())
app.use(corsSetup())
import auth from './router/auth'
import user from './router/user'
import me from './router/me'
import kriteria from './router/kriteria';
import subKriteria from './router/subKriteria';
import perhitungan from './router/perhitungan';
import profil from './router/profil'
import jenis from './router/Jenis'
app.use('/auth', auth)
app.use('/user', user)
app.use('/me', me)
app.use('/kriteria', kriteria)
app.use('/subkriteria', subKriteria)
app.use('/perhitungan', perhitungan)
app.use('/profil', profil)
app.use('/jenis', jenis)
const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
console.log(`App listening on port ${PORT}`)
printRoutes(app, PORT as number);
})

View File

@ -0,0 +1,12 @@
import joi from 'joi'
import {Request, Response, NextFunction} from 'express'
export const bodyValidator = (schema: joi.ObjectSchema) => {
return (req:Request, res:Response, next:NextFunction) => {
const { error } = schema.validate(req.body)
if (error) {
return res.status(400).json({ error: error.details[0].message })
}
next()
}
}

83
BE/src/router/Jenis.ts Normal file
View File

@ -0,0 +1,83 @@
import { Router } from "express";
import { verifyToken } from "../utils/verifyToken";
import { verifyRole } from "../utils/verifyRole";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
const router = Router();
router.get('/', verifyToken, async (req, res) => {
try {
const jenis = await prisma.jenisAlatMusik.findMany();
res.json(jenis);
} catch (error) {
res.status(500).json({ message: "Internal server error" });
}
});
router.get('/:id', verifyToken, async (req, res) => {
const { id } = req.params
try {
const jenis = await prisma.jenisAlatMusik.findUnique({
where: {
id: id
}
})
res.json(jenis)
} catch (error) {
res.status(500).json({ message: 'Internal server error' });
}
})
router.post('/', verifyToken, async (req, res) => {
const { nama } = req.body;
try {
const jenis = await prisma.jenisAlatMusik.create({
data: {
nama
},
});
res.status(201).json(jenis);
} catch (error) {
console.log(error);
res.status(500).json({ message: "Internal server error" });
}
})
router.put('/:id', verifyToken, async (req, res) => {
const { id } = req.params;
const { nama } = req.body;
try {
const jenis = await prisma.jenisAlatMusik.update({
where: {
id: id
},
data: {
nama
}
});
res.json(jenis)
} catch (error) {
console.log(error);
res.status(500).json({ message: "Internal server error" });
}
})
router.delete('/:id', verifyToken, async (req, res) => {
try {
const { id } = req.params;
await prisma.jenisAlatMusik.delete({
where: {
id: id
}
});
res.send("Jenis alat musik berhasil dihapus");
} catch (error) {
console.log(error);
res.status(500).json({ message: "Internal server error" });
}
});
export default router;

78
BE/src/router/auth.ts Normal file
View File

@ -0,0 +1,78 @@
import { Router, Request, Response } from 'express'
import { PrismaClient } from '@prisma/client'
import jwt from 'jsonwebtoken'
import bcrypt from 'bcryptjs'
import { bodyValidator } from '../middleware/body.validator'
import { loginSchema, registrationSchema } from '../schema/auth'
const router = Router()
const prisma = new PrismaClient()
const SECRET_KEY = process.env.SECRET_KEY ?? '';
//@ts-ignore
router.post('/login', bodyValidator(loginSchema), async (req: Request, res: Response) => {
const { username, password } = req.body;
try {
const user = await prisma.user.findFirst({ where: { username: username } });
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({ message: 'Invalid password' });
}
const token = jwt.sign({ id: user.id, username: user.username, role: user.role }, SECRET_KEY, { expiresIn: '1h' });
return res.json({ token });
} catch (e: unknown) {
return res.status(500).json({ message: e });
}
})
//@ts-ignore
router.post('/register', bodyValidator(registrationSchema), async (req: Request, res: Response): Promise<any> => {
const { username, password, email, name } = req.body;
try {
const existingUser = await prisma.user.findFirst({
where: {
username: username
}
});
if (existingUser) {
return res.status(400).json({ message: 'Username is already taken' });
}
const isEmailExist = await prisma.user.findFirst({ where: { email: email } });
if (isEmailExist) {
return res.status(400).json({ message: 'Email is already taken' });
}
// hash password
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
// save user
await prisma.user.create({
data: {
email: email,
username: username,
password: hashedPassword,
name: name
}
});
const token = jwt.sign({ username, email }, SECRET_KEY, { expiresIn: '1h' });
res.status(201).json({ status: 200, message: 'User registered successfully', data: { username, email }, token });
} catch (e: unknown) {
//@ts-ignore
res.status(500).json({ message: e.message });
}
});
export default router

90
BE/src/router/kriteria.ts Normal file
View File

@ -0,0 +1,90 @@
import { Router } from 'express'
import { verifyToken } from '../utils/verifyToken'
import { Criteria, PrismaClient } from '@prisma/client'
import { verifyRole } from '../utils/verifyRole'
const prisma = new PrismaClient()
const router = Router()
//get all kriteria
router.get('/', verifyToken, async (req, res) => {
try {
const kriteria = await prisma.kriteria.findMany()
res.json(kriteria)
} catch (error) {
res.status(500).json({ message: 'Internal server error' });
}
})
// get by id kriteria
router.get('/:id', verifyToken, async (req, res) => {
const { id } = req.params
try {
const kriteria = await prisma.kriteria.findUnique({
where: {
id: id
}
})
res.json(kriteria)
} catch (error) {
res.status(500).json({ message: 'Internal server error' });
}
})
// create kriteria
router.post('/', verifyToken, async (req, res) => {
const { name, code, weight, criteria } = req.body
try {
const kriteria = await prisma.kriteria.create({
data: {
name,
code,
weight,
criteria,
}
})
res.status(201).json(kriteria)
} catch (error) {
console.log(error)
res.status(500).json({ message: 'Internal server error' });
}
})
// update kriteria
router.put('/:id', verifyToken, async (req, res) => {
const { id } = req.params
const { name, code, weight, criteria } = req.body
try {
const kriteria = await prisma.kriteria.update({
where: {
id: id
},
data: {
name,
code,
weight,
criteria,
}
})
res.json(kriteria)
} catch (error) {
console.log(error)
res.status(500).json({ message: 'Internal server error' });
}
})
// delete kriteria
router.delete('/:id', verifyToken, async (req, res) => {
try {
const { id } = req.params
await prisma.kriteria.delete({
where: {
id: id
}
})
res.send('delete kriteria')
} catch (error) {
res.status(500).json({ message: 'Internal server error' });
}
})
export default router

35
BE/src/router/me.ts Normal file
View File

@ -0,0 +1,35 @@
import { Request, Response, Router } from 'express'
import { PrismaClient } from '@prisma/client'
import { verifyToken } from '../utils/verifyToken'
const prisma = new PrismaClient()
const router = Router()
export const me = async (req: Request, res: Response) => {
//@ts-ignore
const userId = req.user?.id; // Ambil id dari req.user
if (!userId) {
res.status(400).json({ message: 'User ID not found in token' });
}
const profile = await prisma.user.findFirst({
where: {
id: userId
},
select: {
username: true,
email: true,
name: true,
role: true
}
})
if (!profile) {
res.status(404).json({ message: 'User not found' });
}
res.json({
status: 200,
message: 'User found',
data: profile
})
}
router.get('/', verifyToken, me)
export default router;

View File

@ -0,0 +1,377 @@
import { PrismaClient } from "@prisma/client";
import { Router } from "express";
import { verifyToken } from "../utils/verifyToken";
import { verifyRole } from "../utils/verifyRole";
const router = Router();
const prisma = new PrismaClient();
router.get('/matrix', verifyToken, verifyRole, async (req, res) => {
try {
// Ambil semua jenis alat musik
const jenisList = await prisma.jenisAlatMusik.findMany({
select: {
nama: true
}
});
const getCriteria = await prisma.kriteria.findMany({
select: { code: true }
});
// Buat struktur data per jenis alat musik
const matrixPerJenis = await Promise.all(
jenisList.map(async (jenis) => {
// Ambil alternatif unik berdasarkan jenis
const alternatifPerJenis = await prisma.subKriteria.findMany({
where: {
namaId: jenis.nama
},
distinct: ['alternatif'],
select: {
alternatif: true,
}
});
const dataTable = await Promise.all(
alternatifPerJenis.map(async (item) => {
const criteria = await prisma.subKriteria.findMany({
where: {
alternatif: item.alternatif,
namaId: jenis.nama
},
select: {
codeId: true,
nilai: true
},
orderBy: {
codeId: 'asc'
}
});
return { alternatif: item.alternatif, criteria };
})
);
return {
jenis: jenis.nama,
dataTable
};
})
);
res.json({
dataHeader: getCriteria,
matrixPerJenis
});
} catch (error) {
console.error(error);
res.status(500).json({ message: 'Internal server error' });
}
});
router.get('/bobot', verifyToken, verifyRole, async (req, res) => {
try {
const bobot = await prisma.kriteria.findMany({
select: {
code: true,
weight: true
}
})
res.json(bobot)
} catch (error: unknown) {
res.status(500).json({ message: error });
}
});
router.get('/normalisasi-bobot', verifyToken, verifyRole, async (req, res) => {
try {
const bobot = await prisma.kriteria.findMany({
select: {
code: true,
weight: true
}
})
const total = bobot.reduce((acc, item) => acc + item.weight, 0);
const bobotNormalized = bobot.map((item) => {
return {
code: item.code,
weight: item.weight / total
}
});
res.json(bobotNormalized)
}
catch (error: unknown) {
res.status(500).json({ message: error });
}
})
router.get('/nilai-utility', verifyToken, verifyRole, async (req, res) => {
try {
// Ambil semua jenis alat musik
const jenisList = await prisma.jenisAlatMusik.findMany({
select: { nama: true }
});
// Iterasi per jenis untuk menghitung utility
const matrixPerJenis = await Promise.all(
jenisList.map(async (jenis) => {
// Ambil semua kriteria
const kriteriaList = await prisma.kriteria.findMany({
orderBy: { code: 'asc' },
include: {
subKriteria: {
where: {
namaId: jenis.nama
}
}
}
});
// Hitung utility per kriteria
const processedKriteria = kriteriaList.map((k) => {
const nilaiList = k.subKriteria.map(e => e.nilai);
const max = Math.max(...nilaiList);
const min = Math.min(...nilaiList);
const subWithUtility = k.subKriteria.map((e) => {
let nilaiUtility = 0;
if (k.criteria === 'COST') {
nilaiUtility = ((max - e.nilai) / (max - min)) * 100;
} else {
nilaiUtility = ((e.nilai - min) / (max - min)) * 100;
}
return { ...e, nilaiUtility: isNaN(nilaiUtility) ? 0 : nilaiUtility };
});
return {
code: k.code,
subKriteria: subWithUtility
};
});
// Ambil alternatif unik
const alternatifList = await prisma.subKriteria.findMany({
where: {
namaId: jenis.nama
},
distinct: ['alternatif'],
select: { alternatif: true }
});
// Gabungkan nilai utility untuk setiap alternatif
const dataTable = alternatifList.map((alt) => {
const criteria = processedKriteria.map((k) => {
const found = k.subKriteria.find(s => s.alternatif === alt.alternatif);
return {
id: found?.id ?? '',
codeId: k.code,
alternatif: alt.alternatif,
nilai: found?.nilai ?? 0,
nilaiUtility: found?.nilaiUtility ?? 0
};
});
return { alternatif: alt.alternatif, criteria };
});
return {
jenis: jenis.nama,
dataHeader: processedKriteria.map(k => k.code),
dataTable
};
})
);
res.json({ matrixPerJenis });
} catch (error) {
console.error(error);
res.status(500).json({ message: 'Internal server error' });
}
});
router.get('/total-nilai', verifyToken, verifyRole, async (req, res) => {
try {
// Ambil semua jenis alat musik
const jenisList = await prisma.jenisAlatMusik.findMany({
select: { nama: true }
});
// Ambil semua kriteria + bobot
const allKriteria = await prisma.kriteria.findMany({
orderBy: { code: 'asc' },
});
const totalBobot = allKriteria.reduce((acc, cur) => acc + cur.weight, 0);
const hasilPerJenis = await Promise.all(jenisList.map(async (jenis) => {
// Ambil semua kriteria dan subkriteria yang sesuai dengan jenis
const kriteriaList = await prisma.kriteria.findMany({
orderBy: { code: 'asc' },
include: {
subKriteria: {
where: { namaId: jenis.nama }
}
}
});
// Proses nilai utility per kriteria
const kriteriaDenganUtility = kriteriaList.map((k) => {
const nilaiList = k.subKriteria.map(s => s.nilai);
const max = Math.max(...nilaiList);
const min = Math.min(...nilaiList);
const isConstant = max === min;
const subKriteriaUtility = k.subKriteria.map(s => {
let nilaiUtility = 0;
if (isConstant) {
nilaiUtility = 100;
} else if (k.criteria === 'COST') {
nilaiUtility = ((max - s.nilai) / (max - min)) * 100;
} else {
nilaiUtility = ((s.nilai - min) / (max - min)) * 100;
}
return { ...s, nilaiUtility };
});
return {
...k,
subKriteria: subKriteriaUtility
};
});
// Ambil alternatif unik berdasarkan jenis
const alternatifList = await prisma.subKriteria.findMany({
where: { namaId: jenis.nama },
distinct: ['alternatif'],
select: { alternatif: true }
});
// Hitung total nilai per alternatif
const dataTable = alternatifList.map((alt) => {
const nilaiTotal = kriteriaDenganUtility.reduce((sum, k) => {
const bobot = k.weight;
const normalisasi = bobot / totalBobot;
const sub = k.subKriteria.find(s => s.alternatif === alt.alternatif);
const utility = sub?.nilaiUtility ?? 0;
return sum + (utility * normalisasi);
}, 0);
return {
alternatif: alt.alternatif,
totalNilai: parseFloat(nilaiTotal.toFixed(2)) // dibulatkan
};
});
return {
jenis: jenis.nama,
data: dataTable.sort((a, b) => b.totalNilai - a.totalNilai)
};
}));
res.status(200).json({ matrixTotal: hasilPerJenis });
} catch (error) {
console.error(error);
res.status(500).json({ message: 'Internal server error' });
}
});
router.get('/ranking', async (req, res) => {
try {
// Ambil semua jenis alat musik
const jenisList = await prisma.jenisAlatMusik.findMany({
select: { nama: true }
});
// Ambil semua kriteria
const allKriteria = await prisma.kriteria.findMany({
orderBy: { code: 'asc' }
});
const totalBobot = allKriteria.reduce((acc, item) => acc + item.weight, 0);
// Proses per jenis
const hasilPerJenis = await Promise.all(
jenisList.map(async (jenis) => {
// Ambil kriteria dengan subkriteria sesuai jenis
const kriteriaList = await prisma.kriteria.findMany({
orderBy: { code: 'asc' },
include: {
subKriteria: {
where: { namaId: jenis.nama }
}
}
});
// Hitung utility
const processedKriteria = kriteriaList.map((k) => {
const nilaiList = k.subKriteria.map(s => s.nilai);
const max = Math.max(...nilaiList);
const min = Math.min(...nilaiList);
const isConstant = max === min;
const subKriteria = k.subKriteria.map((s) => {
let nilaiUtility = 0;
if (isConstant) {
nilaiUtility = 100;
} else if (k.criteria === 'COST') {
nilaiUtility = ((max - s.nilai) / (max - min)) * 100;
} else {
nilaiUtility = ((s.nilai - min) / (max - min)) * 100;
}
return { ...s, nilaiUtility };
});
return { ...k, subKriteria };
});
// Ambil alternatif unik
const alternatifList = await prisma.subKriteria.findMany({
where: { namaId: jenis.nama },
distinct: ['alternatif'],
select: { alternatif: true }
});
// Hitung total nilai per alternatif
const alternatifWithTotal = alternatifList.map((alt) => {
const totalNilai = processedKriteria.reduce((sum, k) => {
const sub = k.subKriteria.find(s => s.alternatif === alt.alternatif);
const utility = sub?.nilaiUtility ?? 0;
const normalisasi = k.weight / totalBobot;
return sum + (utility * normalisasi);
}, 0);
return {
alternatif: alt.alternatif,
totalNilai: parseFloat(totalNilai.toFixed(2)),
};
});
// Ranking per jenis
const ranked = alternatifWithTotal
.sort((a, b) => b.totalNilai - a.totalNilai)
.map((item, index) => ({
...item,
ranking: index + 1,
jenis: jenis.nama
}));
return {
jenis: jenis.nama,
data: ranked
};
})
);
res.status(200).json(hasilPerJenis);
} catch (error) {
console.error(error);
res.status(500).json({ message: 'Internal server error' });
}
});
export default router;

81
BE/src/router/profil.ts Normal file
View File

@ -0,0 +1,81 @@
import { Router } from 'express';
import { verifyToken } from "../utils/verifyToken";
import { verifyRole } from "../utils/verifyRole";
import { PrismaClient } from "@prisma/client"; import { me } from './me';
import { url } from 'inspector';
import { any } from 'joi';
// import { imagekit } from '../utils/utils_image/imagekit';
const router = Router();
const prisma = new PrismaClient();
router.post('/', verifyToken, async (req, res) => {
try {
const { imageUrl } = req.body;
//@ts-ignore
const userId = req.user?.id; // Ambil ID dari token
if (!imageUrl) {
res.status(400).json({ message: 'Image URL is required' });
return
}
if (!userId) {
res.status(401).json({ message: 'User ID not found in token' });
return
}
const savedImage = await prisma.user.update({
where: { id: userId },
data: {
photo: imageUrl,
},
});
res.status(201).json(savedImage);
} catch (error) {
res.status(500).json({ message: 'Internal server error' });
}
});
router.get('/', verifyToken, async (req, res) => {
try {
//@ts-ignore
const userId = req.user?.id; // Ambil ID dari token
if (!userId) {
res.status(401).json({ message: 'User ID not found in token' });
return
}
const user = await prisma.user.findUnique({
where: { id: userId },
});
if (!user) {
res.status(404).json({ message: 'User not found' });
return
}
res.json(user);
} catch (error) {
res.status(500).json({ message: 'Internal server error' });
}
})
router.delete('/', verifyToken, async (req, res) => {
try {
await prisma.user.update({
//@ts-ignore
where: { id: req.user?.id },
data: {
photo: "",
},
})
res.status(200).json({ message: 'Image deleted successfully' });
} catch (error) {
res.status(500).json({ message: error });
}
})
export default router;

6
BE/src/router/rank.ts Normal file
View File

@ -0,0 +1,6 @@
import { Router } from "express";
import { verifyToken } from "../utils/verifyToken";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
// const router =

View File

@ -0,0 +1,109 @@
import { Router } from "express";
import { verifyToken } from "../utils/verifyToken";
import { subKriteria, PrismaClient } from "@prisma/client";
import { verifyRole } from "../utils/verifyRole";
const prisma = new PrismaClient();
const router = Router();
//get all subkriteria
router.get("/", verifyToken, async (req, res) => {
try {
const subkriteria = await prisma.subKriteria.findMany();
res.json(subkriteria);
} catch (error) {
res.status(500).json({ message: "Internal server error" });
}
})
//insert data subkriteria
router.post('/', verifyToken, async (req, res) => {
const { alternatif, codeId, nilai, namaId } = req.body
try {
const isExist = await prisma.subKriteria.findFirst({
where: { codeId, alternatif }
})
if (isExist) throw { message: 'Subkriteria already exist' }
const getCriteria = await prisma.kriteria.findUnique({
where: {
code: codeId
}
})
if (!getCriteria) throw { message: 'Kriteria not found' }
const subkriteria = await prisma.subKriteria.create({
data: {
alternatif,
nilai: parseInt(nilai),
jenis: {
connect: {
nama: namaId
}
},
kriteria: {
connect: {
code: codeId
}
}
}
})
res.status(201).json(subkriteria)
} catch (error: any) {
console.log(error)
res.status(500).json({ message: 'Internal server error', error });
}
})
//update data subkriteria
router.put('/:id', verifyToken, async (req, res) => {
const { alternatif, codeId, nilai, namaId } = req.body
const { id } = req.params
try {
const subKriteria = await prisma.subKriteria.update({
where: {
id: id
},
data: {
alternatif,
nilai,
jenis: {
connect: {
nama: namaId
}
},
kriteria: {
connect: {
code: codeId
}
}
}
}
)
res.json(subKriteria)
} catch (error) {
console.log(error)
res.status(500).json({ message: 'Internal server error' });
}
})
//delete data subkriteria
router.delete('/:id', verifyToken, async (req, res) => {
const { id } = req.params
try {
const existingSubKriteria = await prisma.subKriteria.findUnique({
where: { id }
});
if (!existingSubKriteria) throw { message: 'Subkriteria not found' }
await prisma.subKriteria.delete({
where: {
id
}
});
res.json({ message: 'Delete success' })
} catch (error) {
console.log(error)
res.status(500).json({ message: 'Internal server error', error });
}
})
export default router;

46
BE/src/router/user.ts Normal file
View File

@ -0,0 +1,46 @@
import { PrismaClient } from '@prisma/client'
import { Router, Request, Response, NextFunction } from 'express'
import { verifyToken } from '../utils/verifyToken'
import { ResponseDTO } from '../types'
const router = Router()
const prisma = new PrismaClient()
//get one user
//@ts-ignore
router.get('/:id', verifyToken, async (req: Request, res: Response, next: NextFunction): Promise<any> => {
const { id } = req.params
const user = await prisma.user.findUnique({
where: { id },
omit: {
password: true,
id: true
}
})
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
return res.json(user)
})
//get all users
//@ts-ignore
router.get('/', verifyToken, async (req, res):Promise<ResponseDTO> => {
try {
const users = await prisma.user.findMany({
select: {
id: true,
name: true,
username: true,
email: true, }
})
res.json({
status: 200,
message: 'Users found',
data: users
})
}catch (error) {
res.status(500).json({ message: 'Internal server error' });
}
})
export default router

13
BE/src/schema/auth.ts Normal file
View File

@ -0,0 +1,13 @@
import Joi from 'joi';
export const registrationSchema = Joi.object({
name: Joi.string().min(3).max(30).required(),
email: Joi.string().email().required(),
username: Joi.string().min(3).max(30).required(),
password: Joi.string().min(6).max(30).required(),
});
export const loginSchema = Joi.object({
username: Joi.string().min(3).max(30).required(),
password: Joi.string().min(6).max(30).required(),
});

7
BE/src/services/auth.ts Normal file
View File

@ -0,0 +1,7 @@
export const signIn = async (email: string, password: string) => {
//
}
export const signUp = async (email: string, password: string) => {
//
console.log('test')
}

9
BE/src/types/index.ts Normal file
View File

@ -0,0 +1,9 @@
import { User } from "@prisma/client";
export interface UserDTO extends User {}
export interface ResponseDTO<T> {
status: number;
message: string;
data: T;
}

View File

@ -0,0 +1,75 @@
import express from 'express';
import expressListEndpoints from 'express-list-endpoints';
// Reset = "\x1b[0m"
// Bright = "\x1b[1m"
// Dim = "\x1b[2m"
// Underscore = "\x1b[4m"
// Blink = "\x1b[5m"
// Reverse = "\x1b[7m"
// Hidden = "\x1b[8m"
// FgBlack = "\x1b[30m"
// FgRed = "\x1b[31m"
// FgGreen = "\x1b[32m"
// FgYellow = "\x1b[33m"
// FgBlue = "\x1b[34m"
// FgMagenta = "\x1b[35m"
// FgCyan = "\x1b[36m"
// FgWhite = "\x1b[37m"
// FgGray = "\x1b[90m"
// BgBlack = "\x1b[40m"
// BgRed = "\x1b[41m"
// BgGreen = "\x1b[42m"
// BgYellow = "\x1b[43m"
// BgBlue = "\x1b[44m"
// BgMagenta = "\x1b[45m"
// BgCyan = "\x1b[46m"
// BgWhite = "\x1b[47m"
// BgGray = "\x1b[100m"
export const colorConsole = (color: string, message: string) => {
let colorCode = '';
switch (color) {
case 'red':
colorCode = '\x1b[31m';
break;
case 'green':
colorCode = '\x1b[32m';
break;
case 'yellow':
colorCode = '\x1b[33m';
break;
case 'blue':
colorCode = '\x1b[34m';
break;
case 'magenta':
colorCode = '\x1b[35m';
break;
case 'cyan':
colorCode = '\x1b[36m';
break;
case 'white':
colorCode = '\x1b[37m';
break;
case 'gray':
colorCode = '\x1b[90m';
break;
default:
colorCode = '\x1b[37m';
break;
}
console.log(colorCode + message);
}
export const printRoutes = (app: express.Express, port: number) => {
const endpoints = expressListEndpoints(app);
console.log('\x1b[34m' + 'Registered Routes:');
endpoints.forEach((endpoint) => {
const methods = endpoint.methods.join(', ').toUpperCase();
const path = `http://localhost:${port}${endpoint.path}`;
colorConsole('green', `${path} ${methods}`);
});
};

43
BE/src/utils/seedAdmin.ts Normal file
View File

@ -0,0 +1,43 @@
import { PrismaClient } from '@prisma/client'
import bcrypt from 'bcryptjs'
const prisma = new PrismaClient()
const seedAdmin = async () => {
console.log('setup admin')
const passwordDefault = 'admin1234'
const salt = await bcrypt.genSalt(10)
const hash = await bcrypt.hash(passwordDefault, salt)
await prisma.user.upsert({
where: { email: 'admin@test.com' },
update: {},
create: {
email: 'admin@test.com',
password: hash,
username: "admin",
name: "admin",
role: "ADMIN"
}
})
const getAdmin = await prisma.user.findFirst({
where: {
username: 'admin'
},
select: {
username: true,
email: true,
role: true
}
})
return console.log(getAdmin)
}
seedAdmin().catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@ -0,0 +1,41 @@
import { Role } from "@prisma/client";
import { NextFunction, Request, Response } from "express";
import jwt, { JwtPayload } from "jsonwebtoken";
const SECRET_KEY = process.env.SECRET_KEY ?? '';
interface CustomRequest extends Request {
user?: string | JwtPayload;
}
interface DecodeDTO {
id: string,
username: string,
role: Role,
iat: number,
exp: number
}
export const verifyRole = (req: Request, res: Response, next: NextFunction) => {
const token = req.headers['authorization'];
if (!token) {
res.status(403).json({ message: 'No token provided' });
return
}
try {
jwt.verify(token?.split(' ')[1]!, SECRET_KEY, (err: any, decoded: any) => {
if (err) {
res.status(401).json({ message: 'Unauthorized' });
return
}
(req as CustomRequest).user = decoded;
const decodeDto = decoded as DecodeDTO
if(decodeDto.role !== Role.ADMIN) {
res.status(403).json({ message: 'Forbidden only for admin' });
return
}
next();
});
} catch (error) {
res.status(500).json({ message: 'Internal server error' });
return
}
}

View File

@ -0,0 +1,29 @@
import { NextFunction, Request, Response } from "express";
import jwt, { JwtPayload } from "jsonwebtoken";
const SECRET_KEY = process.env.SECRET_KEY ?? '';
interface CustomRequest extends Request {
user?: string | JwtPayload;
}
export const verifyToken = (req: Request, res: Response, next: NextFunction) => {
const token = req.headers['authorization'];
// console.log("token di verify", token);
if (!token) {
res.status(403).json({ message: 'No token provided' });
return
}
try {
jwt.verify(token?.split(' ')[1]!, SECRET_KEY, (err: any, decoded: any) => {
if (err) {
res.status(401).json({ message: 'Unauthorized' });
return
}
(req as CustomRequest).user = decoded;
next();
});
} catch (error) {
res.status(500).json({ message: 'Internal server error' });
return
}
}

103
BE/tsconfig.json Normal file
View File

@ -0,0 +1,103 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "ES2015", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
"rootDir": "./src", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
"sourceMap": false, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist", /* Specify an output folder for all emitted files. */
"removeComments": true, /* Disable emitting comments. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

24
FE/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

50
FE/README.md Normal file
View File

@ -0,0 +1,50 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
```js
// eslint.config.js
import react from 'eslint-plugin-react'
export default tseslint.config({
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
})
```

28
FE/eslint.config.js Normal file
View File

@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

17
FE/index.html Normal file
View File

@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sistem Pendukung Keputusan</title>
</head>
<body>
<div id="root">
</div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4297
FE/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

40
FE/package.json Normal file
View File

@ -0,0 +1,40 @@
{
"name": "sistem-pendukung-keputusan2",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview --host"
},
"dependencies": {
"@tanstack/react-query": "^5.66.9",
"axios": "^1.7.9",
"imagekit": "^6.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.54.2",
"react-icons": "^5.4.0",
"zustand": "^5.0.3"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/cors": "^2.8.17",
"@types/node": "^22.10.7",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"eslint": "^9.17.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
"postcss": "^8.5.1",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"typescript-eslint": "^8.18.2",
"vite": "^6.0.5"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

BIN
FE/public/assets/musik.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
FE/public/assets/musik3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

1
FE/public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

32
FE/src/App.tsx Normal file
View File

@ -0,0 +1,32 @@
import { Carousel } from './components/Carousel'
import { Navbar } from './components/Navbar'
import { AboutUs } from './components/AboutUs'
import Dashboard from './components/pages/Dashboard'
import { usePage } from './hooks/usePage'
function App() {
const {page, setPage} = usePage()
console.log(page)
return (
<>
{page === 'dashboard' && (
<Dashboard />
)}
{
page !== 'dashboard' && (
<>
<Navbar setCurrentPage={setPage} />
{page === 'home' && <Carousel goToDashboard={() => setPage('dashboard')} />}
{page === 'about' && <AboutUs />}
</>
)
}
{/* <Carousel /> */}
</>
)
}
export default App

BIN
FE/src/assets/Lifestyle.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

BIN
FE/src/assets/Outdoor.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

BIN
FE/src/assets/musik.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

BIN
FE/src/assets/musik2.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
FE/src/assets/musik3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

1
FE/src/assets/react.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

@ -0,0 +1,33 @@
// import { Navbar } from "./Navbar"
export const AboutUs = () => {
return (
<div className="bg-black min-h-screen flex flex-col items-center justify-center text-black">
<div className="max-w-4xl p-5 bg-yellow-300 shadow-lg rounded-lg mb-20 text-center mt-24">
<div className="flex flex-row justify-center">
<h1 className="text-4xl font-bold mb-5 text-center text-black hover:text-white duration-200 hover-scale-up">About Us</h1>
</div>
<p className="text-lg mb-3">
Sistem Pendukung Keputusan (SPK) adalah suatu sistem yang membantu pengambilan keputusan dalam suatu organisasi atau perusahaan. SPK ini dibuat dengan
menggunakan metode SMART (Simple Multi Atribute Rating Technique).
</p>
<p className="text-lg mb-3">
Sistem ini dibuat berdasarkan sepenuhnya ide dari penulis (peneliti), yang akan membantu banyak orang dalamm memilih
pilihan mereka. Sistem ini sangat cocok bagi kaum yang kesulitan dalam memilih suatu keputusan, atau bagi kaum mendang-mending.
</p>
<div className="mt-14 mb-5 flex flex-col items-center">
<h4 className="text-2xl font-semibold">Contact Us</h4>
<ul className="flex flex-col items-center justify-center">
<li className=" text-black py-2 px-4">Phone: 082363159160</li>
<li className=" text-black py-2 px-4">Email: hilmibarca24@gmail.com</li>
</ul>
<ul className="flex flex-col items-center justify-center mt-4">
<p className="text-black">Admin Access</p>
<li className="text-black">Username: admin</li>
<li className="text-black">password: admin1234</li>
</ul>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,41 @@
import { useState } from 'react';
import { FormLogin } from './FormLogin';
import { FormRegister } from './FormRegister';
export const AuthPage = () => {
const [isNewMember, setIsNewMember] = useState(false);
return (
<div className='flex h-screen bg-neutral-800 text-white relative overflow-hidden'>
<div className='absolute w-[1080px] h-[1080px] bg-yellow-500 rounded-full flex items-center justify-center -left-[567px] -top-[80px]'>
<div className='absolute w-[500px] h-[500px] bg-blue-600 rounded-full'></div>
</div>
<div className='flex flex-col justify-center w-full md:w-1/2 absolute md:right-0 h-full'>
{isNewMember ? (
<FormRegister>
<div className='flex items-center space-x-1'>
<p className='mt-2'>Sudah punya akun?</p>
<p
className='text-yellow-600 cursor-pointer hover:text-yellow-700 mt-2'
onClick={() => setIsNewMember(false)}
>
Masuk
</p>
</div>
</FormRegister>
) : (
<FormLogin>
<div className='flex items-center space-x-1'>
<p className='mt-2'>Belum punya akun?</p>
<p
className='text-yellow-600 cursor-pointer hover:text-yellow-700 mt-2'
onClick={() => setIsNewMember(true)}
>
Daftar
</p>
</div>
</FormLogin>
)}
</div>
</div>
);
};

View File

@ -0,0 +1,37 @@
import { useGetBobotCriteria } from "../hooks/perhitungan/useGetBobotCriteria";
export const BobotKriteria = () => {
const { data, isLoading, isError } = useGetBobotCriteria();
if (isLoading) return <div className="text-center">Loading...</div>;
if (isError) return <div className="text-center">Error loading data</div>;
return (
<div className="w-full p-8 bg-white border border-gray-200 rounded-lg shadow-md">
<div className="flex items-center justify-between mb-2">
<h2 className="text-lg font-semibold text-blue-400">Bobot Kriteria (W)</h2>
{/* <button className="items-end bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600 mb-2">Tambah Data</button> */}
</div>
<div className="flex justify-center">
<table className="border border-collapse border-gray-500 w-full">
<thead>
<tr className="bg-yellow-300 text-black">
{data?.map((item, index) => (
<th key={index} className="border border-gray-500">
{item.code}
</th>
))}
</tr>
</thead>
<tbody className="text-center">
<tr>
{data?.map((item, index) => (
<td key={index} className="border border-gray-500">
{item.weight}
</td>
))}
</tr>
</tbody>
</table>
</div>
</div>
);
}

View File

@ -0,0 +1,104 @@
import { useState, useEffect } from "react";
// import { Navbar } from './Navbar'
interface CarouselProps {
goToDashboard?: () => void;
}
export const Carousel: React.FC<CarouselProps> = ({ goToDashboard }) => {
const slides = [
{
id: 1,
src: "./assets/Lifestyle.jpg",
bg: "./assets/Lifestyle.jpg",
title: "Lifestyle",
description: "Learn about music for your lifestyle dan make a history.",
buttonText: "Get started",
},
{
id: 2,
src: "./assets/Musik_and_Sports.jpg",
bg: "./assets/Musik_and_Sports.jpg",
title: "Music & Sports",
description: "Combine your music with a good and regular sports life.",
buttonText: "Get Started",
},
{
id: 3,
src: "./assets/Outdoor.jpg",
bg: "./assets/Outdoor.jpg",
title: "Outdoor",
description: "Feel the sensation of the song with you and reminisce outdoors.",
buttonText: "Get Started",
},
];
const [currentSlide, setCurrentSlide] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCurrentSlide((prev) => (prev + 1) % slides.length);
}, 5000);
return () => clearInterval(interval);
}, [slides.length]);
// const nextSlide = () => {
// setCurrentSlide((prev) => (prev + 1) % slides.length);
// };
// const prevSlide = () => {
// setCurrentSlide((prev) => (prev - 1 + slides.length) % slides.length);
// };
return (
<div
className="relative w-full h-screen bg-cover bg-center overflow-hidden transform transition-all duration-700 ease-linier"
style={{
backgroundImage: `url(${slides[currentSlide].bg})`,
}}
>
{/* <Navbar /> */}
{/* Overlay */}
<div className="absolute inset-0 bg-black bg-opacity-50 z-10"></div>
{/* Content */}
<div className="relative z-20 max-w-5xl mx-auto h-5/6 flex flex-col justify-center items-start px-6 text-white">
<h1 className="text-5xl font-bold mb-4">{slides[currentSlide].title}</h1>
<p className="text-lg mb-6">{slides[currentSlide].description}</p>
<button className="bg-yellow-500 hover:bg-yellow-700 text-white px-6 py-3 rounded-lg" onClick={goToDashboard}>
{slides[currentSlide].buttonText}
</button>
</div>
{/* Image Thumbnails */}
<div className="relative bottom-10 left-1/2 transform translate-x-60 flex space-x-5 z-20">
{slides.map((slide, index) => (
<img
key={slide.id}
src={slide.src}
alt={`Thumbnail ${index + 1}`}
className={`w-24 h-32 rounded-xl shadow-lg transform transition duration-700 ease-in-out ${index === currentSlide ? "scale-110" : "scale-100 hover:scale-125"
}`}
onClick={() => setCurrentSlide(index)}
/>
))}
</div>
{/* Navigation Buttons */}
{/* <button
onClick={prevSlide}
className="absolute left-4 top-1/2 -translate-y-1/2 bg-black bg-opacity-50 text-white p-3 rounded-full z-20 hover:bg-opacity-75"
>
</button>
<button
onClick={nextSlide}
className="absolute right-4 top-1/2 -translate-y-1/2 bg-black bg-opacity-50 text-white p-3 rounded-full z-20 hover:bg-opacity-75"
>
</button> */}
</div>
);
};

View File

@ -0,0 +1,29 @@
interface EditCriteriaProps {
isOpen: boolean;
onClickClose: () => void;
children?: React.ReactNode;
title?: string;
}
export const EditCriteria: React.FC<EditCriteriaProps> = (props) => {
const { isOpen: open, onClickClose, children, title } = props;
// if (data) {
return (
<div className={`fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50 ${open ? 'block' : 'hidden'}`}>
<div className="absolute left-1/2 top-1/2 md:w-[80%] transform -translate-x-1/2 -translate-y-1/2">
<div className="bg-white p-8 rounded-lg shadow-md border">
{/* Close Button */}
<button className="absolute top-2 right-2 text-gray-400 hover:text-black p-2" onClick={onClickClose}>
X
</button>
<h2 className="text-lg font-semibold mb-3 text-gray-500">{title}</h2>
{children}
</div>
</div>
</div>
)
// }
// return null
}

View File

@ -0,0 +1,25 @@
interface EditSubCriteriaProps {
isOpen: boolean;
onClickClose: () => void;
children?: React.ReactNode;
title?: string;
}
export const EditSubCriteria: React.FC<EditSubCriteriaProps> = (props) => {
const { isOpen: open, onClickClose, children, title } = props;
return (
< div className={`fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50 ${open ? 'block' : 'hidden'}`}>
<div className="absolute left-1/2 top-1/2 md:w-[80%] transform -translate-x-1/2 -translate-y-1/2">
<div className="bg-white p-8 rounded-lg shadow-md border">
{/* Close Button */}
<button className="absolute top-2 right-2 text-gray-400 hover:text-black p-2" onClick={onClickClose}>
X
</button>
<h2 className="text-lg font-semibold mb-3 text-gray-500">{title}</h2>
{children}
</div>
</div>
</div >
)
}

View File

@ -0,0 +1,45 @@
import { useForm } from "react-hook-form";
import { Jenis, useGetJenisQuery } from "../hooks/jenis/useGetJenisQuery";
import { useUpdateJenisMutation } from "../hooks/jenis/useUpdateJenisMutation";
import React from "react";
interface FormEditJenisProps {
onClickClose: () => void;
data: Jenis;
}
export const FormEditJenis: React.FC<FormEditJenisProps> = ({ onClickClose, data }) => {
const { mutateAsync } = useUpdateJenisMutation();
const { refetch } = useGetJenisQuery();
const { register, formState: { errors }, handleSubmit } = useForm<Jenis>({ values: data });
const submit = async (data: Jenis) => {
try {
await mutateAsync({ ...data, id: data.id });
refetch();
onClickClose();
} catch (e) {
console.error(e);
}
};
return (
<form className="w-full" onSubmit={handleSubmit(submit)}>
<div className="grid grid-cols-1 md:grid-cols-1 gap-6 mb-6">
<div className="flex-1 min-w-[200px]">
<label className="block mb-1 text-sm font-medium text-gray-700">Nama Jenis Alat Musik</label>
<input type="text" className="w-full p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500" placeholder="Nama Jenis" {...register('nama', { required: 'Nama tidak boleh kosong!' })} />
{errors.nama && <span className="text-red-500">{errors.nama.message}</span>}
</div>
</div>
<div className="gap-3 flex justify-end">
<button type="submit" className="bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600">
Simpan
</button>
<button type="reset" className="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600">
Reset
</button>
</div>
</form>
);
}

View File

@ -0,0 +1,81 @@
import { useForm } from "react-hook-form";
import { Criteria, useGetCriteriaQuery } from "../hooks/criteria/useGetCriteriaQuery";
import { useUpdateCriteriaMutation } from "../hooks/criteria/useUpdateCriteriaMutation";
import React from "react";
import { MdSaveAlt } from "react-icons/md";
import { BiReset } from "react-icons/bi";
interface FormEditKriteriaProps {
onClickClose: () => void,
data: Criteria
}
export const FormEditKriteria: React.FC<FormEditKriteriaProps> = ({ onClickClose, data }) => {
const { mutateAsync } = useUpdateCriteriaMutation()
const { refetch } = useGetCriteriaQuery()
const { register, formState: { errors }, handleSubmit } = useForm<Criteria>({ values: data });
const submit = async (data: Criteria) => {
try {
const normalize = { ...data, weight: Number(data.weight) }
await mutateAsync(normalize)
refetch()
onClickClose()
} catch (e) {
console.log(e)
}
}
return (
<form className="w-full" onSubmit={handleSubmit(submit)}>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
{/* Kode Kriteria */}
<div className="flex-1 min-w-[200px]">
<label className="block mb-1 text-sm font-medium text-gray-700">Kode Kriteria</label>
<input type="text" className="w-full p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500" placeholder="Kode" {...register('code', {
required: 'Kode tidak boleh kosong!',
minLength: { value: 1, message: 'Kode minimal 3 karakter!' },
maxLength: { value: 2, message: 'Kode maksimal 2 karakter!' }
})} />
{errors.code && <span className="text-red-500">{errors.code.message}</span>}
</div>
{/* Nama Kriteria */}
<div className="flex-1 min-w-[200px]">
<label className="block mb-1 text-sm font-medium text-gray-700">Nama Kriteria</label>
<input type="text" className="w-full p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500" placeholder="Status" {...register('name', { required: 'Nama tidak boleh kosong!' })} />
{/* {errors.name && <span className="text-red-500">{errors.name.message}</span>} */}
</div>
{/* Bobot Kriteria */}
<div className="flex-1 min-w-[200px]">
<label className="block mb-1 text-sm font-medium text-gray-700">Bobot Kriteria</label>
<input type="number" className="w-full p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500" placeholder="Masukkan bobot kriteria" {...register('weight', { required: 'Bobot tidak boleh kosong!' })} />
{/* {errors.weight && <span className="text-red-500">Bobot tidak boleh kosong!</span>} */}
</div>
{/* Jenis Kriteria */}
<div className="flex-1 min-w-[200px]">
<label className="block mb-1 text-sm font-medium text-gray-700">Jenis Kriteria</label>
<select className="w-full p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500" {...register('criteria', { required: 'Jenis tidak boleh kosong!' })} >
<option value="">--Pilih Jenis Kriteria--</option>
<option value="BENEFIT" >Benefit</option>
<option value="COST">Cost</option>
</select>
</div>
</div>
{/* Tombol Simpan & Reset */}
<div className="flex justify-end space-x-4">
<button type="submit" className="bg-green-500 text-white px-6 py-3 rounded-lg hover:bg-green-600 flex items-center">
<MdSaveAlt className="mr-1" /> <span>Simpan</span>
</button>
<button type="reset" className="bg-blue-500 text-white px-6 py-3 rounded-lg hover:bg-blue-600 flex items-center">
<BiReset className="mr-1" /> <span>Reset</span>
</button>
</div>
</form>
)
}

View File

@ -0,0 +1,161 @@
import { useForm } from "react-hook-form";
import { useUpdateSubCriteriaMutation, } from "../hooks/subKriteria/useUpdateSubCriteriaMutation";
import { useGetSubCriteriaQuery } from "../hooks/subKriteria/useGetSubCriteriaMutation"; // Perbaikan import
import { BiExit } from "react-icons/bi";
import { MdSaveAlt } from "react-icons/md";
import { useEffect, useMemo } from "react";
import { useGetJenisQuery } from "../hooks/jenis/useGetJenisQuery";
interface FormEditSubCriteriaProps {
onClickClose: () => void;
subKey: string
}
interface FormValues {
alternatif: string;
value: string;
namaId: string;
nilai: number;
id: string
}
export const FormEditSubCriteria: React.FC<FormEditSubCriteriaProps> = ({ subKey, onClickClose }) => {
const { mutateAsync } = useUpdateSubCriteriaMutation();
const { refetch, data } = useGetSubCriteriaQuery();
const { data: jenisData } = useGetJenisQuery();
const dataMemo = useMemo(() => {
if (!data || !subKey) return [];
return data?.filter(item => item.alternatif === subKey);
}, [data, subKey]);
const { register, handleSubmit, reset, watch, setValue, setError, formState: { errors } } = useForm<FormValues>({
defaultValues: {
alternatif: subKey,
value: '',
nilai: 0,
},
});
const selectedCode = watch('value');
useEffect(() => {
const selectedItem = dataMemo.find(item => item.codeId === selectedCode);
if (selectedItem) {
setValue("nilai", selectedItem.nilai);
setValue("id", selectedItem.id);
} else {
setValue("nilai", 0)
setValue("id", '')
};
}, [selectedCode, dataMemo, setValue]);
const onSubmit = async (formData: FormValues) => {
try {
const oldData = dataMemo.find(item => item.id === formData.id);
const isAlterNatifeNoChange = formData.alternatif === subKey;
const isNoChange = formData.nilai === oldData?.nilai || formData.nilai === 0;
const isJenisNoChange = formData.namaId === oldData?.namaId;
const isCodeNoChange = isNoChange && isAlterNatifeNoChange && isJenisNoChange;
if (isCodeNoChange) {
setError('alternatif', {
type: 'manual',
message: 'Tidak ada perubahan data'
});
setError('nilai', {
type: 'manual',
message: 'Tidak ada perubahan data'
});
return
}
if (!isNoChange && !isAlterNatifeNoChange) {
console.log('nyantol boos')
await mutateAsync({ alternatif: formData.alternatif, codeId: formData.value, nilai: formData.nilai, id: formData.id, namaId: formData.namaId });
reset();
refetch();
onClickClose();
} else if (!isAlterNatifeNoChange && isNoChange) {
await Promise.all(dataMemo.map(async (item) => {
await mutateAsync({ alternatif: formData.alternatif, codeId: item.codeId, nilai: item.nilai, id: item.id, namaId: item.namaId });
}))
reset();
refetch();
onClickClose();
} else {
await mutateAsync({ alternatif: formData.alternatif, codeId: formData.value, nilai: formData.nilai, id: formData.id, namaId: formData.namaId });
reset();
refetch();
onClickClose();
}
} catch (error) {
console.log("Error creating item:", error);
}
};
useEffect(() => {
reset({
alternatif: subKey,
})
}, [subKey])
return (
<form className="w-full" onSubmit={handleSubmit(onSubmit)}>
<div className="grid grid-cols-1 gap-3 mb-3">
<div className="flex-1 min-w-[200px]">
<label className="block mb-1 text-sm font-medium text-gray-700">Nama Alternatif</label>
<input
type="text"
className={"w-full p-3 border border-gray-500 rounded-lg"} placeholder="Nama Alternatif"
{...register("alternatif", { required: "Nama alternatif wajib diisi" })}
/>
<div className="flex-1 min-w-[200px] my-3">
<label className="block mb-1 text-sm font-medium text-gray-700">Jenis</label>
<select className="w-full p-3 border border-gray-500 rounded-lg focus:ring-blue-500"
{...register("namaId", { required: "Jenis alat musik wajib dipilih" })}>
<option value="">-- Pilih Jenis --</option>
{jenisData?.map((jenis, index) => (
<option key={index} value={jenis.nama}>{jenis.nama}</option>
))}
</select>
</div>
<div className="flex-1 min-w-[200px] my-3">
<label className="block mb-1 text-sm font-medium text-gray-700">Kode Criteria</label>
<select className="w-full p-3 border border-gray-500 rounded-lg focus:ring-blue-500" {...register("value")}>
<option value="" className="text-gray-500">-Select Code-</option>
{dataMemo.map((item, index) => (
<option key={index} value={item.codeId}>{item.codeId}</option>
))}
</select>
</div>
<div className="flex-1 min-w-[200px] my-3">
<label className="block mb-1 text-sm font-medium text-gray-700">Nilai Sub-Criteria</label>
<input
type="number"
className={"w-full p-3 border border-gray-500 rounded-lg disabled:cursor-not-allowed"} placeholder="Nilai Alternatif"
{...register("nilai", { required: "Nilai alternatif wajib diisi", valueAsNumber: true, disabled: selectedCode === '' })}
/>
</div>
<span>{errors.alternatif && errors.alternatif.message || errors.nilai && errors.nilai.message}</span>
</div>
</div>
<div className="flex justify-end space-x-4">
<button type="submit" className="bg-green-500 text-white px-6 py-3 rounded-lg flex items-center">
<MdSaveAlt className="mr-1" /> Simpan
</button>
<button
type="button"
className="bg-blue-500 text-white px-6 py-3 rounded-lg flex items-center"
onClick={onClickClose}
>
<BiExit className="mr-1" /> Close
</button>
</div>
</form>
);
};

View File

@ -0,0 +1,91 @@
import { SubmitHandler, useForm } from 'react-hook-form';
import { usePopup } from '../hooks/usePopUp';
import axios from 'axios';
import { useLogin } from '../hooks/useLogin';
import { ReactNode } from 'react';
type FormValues = {
username: string;
password: string;
};
export const FormLogin: React.FC<{ children: ReactNode }> = ({ children }) => {
const { showPopup } = usePopup();
const { me } = useLogin();
const {
register,
formState: { errors },
handleSubmit,
} = useForm<FormValues>();
const onSubmit: SubmitHandler<FormValues> = async (data) => {
try {
const response = await axios.post('http://localhost:3000/auth/login', {
username: data.username,
password: data.password,
});
localStorage.setItem('token', response.data.token);
me();
// setIsAuthenticated(true);
} catch (e: unknown) {
//@ts-expect-error: Type of error is unknown
const error = e?.response?.data?.error;
//@ts-expect-error: Type of error is unknown
const message = e?.response?.data?.message;
if (error) {
showPopup(error);
}
if (message) {
showPopup(message);
}
}
};
return (
<>
<form
className='w-full flex items-center justify-left relative z-10'
onSubmit={handleSubmit(onSubmit)}
>
<div className='flex flex-col p-4 w-full rounded-lg xl:w-1/2 bg-neutral-800 mx-2'>
<h1 className='text-7xl font-livvic text-neutral-200 mb-8'>LOGIN</h1>
<div className='flex flex-col'>
<input
type='text'
placeholder='Username'
className='bg-transparent w-full py-2 px-4 rounded mt-4 border-neutral-400 border'
{...register('username', {
required: true,
minLength: 3,
maxLength: 20,
})}
/>
{errors.username && (
<span className='text-red-500'>Username is required</span>
)}
<input
type='password'
placeholder='Password'
className='bg-transparent w-full py-2 px-4 rounded mt-6 border-neutral-400 border'
{...register('password', {
required: true,
minLength: 3,
maxLength: 20,
})}
/>
{errors.password && (
<span className='text-red-500'>Password is required</span>
)}
{children}
<button
type='submit'
className='bg-yellow-500 uppercase font-bold mt-8 rounded-lg p-2 text-neutral-800 hover:bg-yellow-600'
>
Login
</button>
</div>
</div>
</form>
</>
);
};

View File

@ -0,0 +1,123 @@
import { SubmitHandler, useForm } from 'react-hook-form';
import { usePopup } from '../hooks/usePopUp';
import axios from 'axios';
import { useLogin } from '../hooks/useLogin';
import { ReactNode } from 'react';
type FormValues = {
username: string;
password: string;
email: string;
name: string;
};
export const FormRegister: React.FC<{ children?: ReactNode }> = ({
children,
}) => {
const { showPopup } = usePopup();
const { setIsAuthenticated, me } = useLogin();
const {
register,
formState: { errors },
handleSubmit,
} = useForm<FormValues>();
const onSubmit: SubmitHandler<FormValues> = async (data) => {
try {
const response = await axios.post('http://localhost:3000/auth/register', {
username: data.username,
password: data.password,
email: data.email,
name: data.name,
});
localStorage.setItem('token', response.data.token);
me();
setIsAuthenticated(true);
} catch (e: unknown) {
//@ts-expect-error: Type of error is unknown
const error = e?.response?.data?.error;
//@ts-expect-error: Type of error is unknown
const message = e?.response?.data?.message;
if (error) {
showPopup(error);
}
if (message) {
showPopup(message);
}
// console.log(e?.response?.data.error);
}
};
return (
<>
<form
className='w-full flex items-center justify-left relative z-10'
onSubmit={handleSubmit(onSubmit)}
>
<div className='flex flex-col p-4 w-full rounded-lg xl:w-1/2 bg-neutral-800 mx-2'>
<h1 className='text-7xl font-livvic text-neutral-200 mb-8'>REGISTER</h1>
<div className='flex flex-col'>
<input
type='text'
placeholder='Name'
{...register('name', {
required: true,
minLength: 3,
maxLength: 20,
})}
className='bg-transparent w-full py-2 px-4 rounded mt-4 border-neutral-400 border'
/>
{errors.name && (
<span className='text-red-500'>Name is required</span>
)}
<input
type='email'
placeholder='Email'
className='bg-transparent w-full py-2 px-4 rounded mt-6 border-neutral-400 border'
{...register('email', {
required: true,
minLength: 3
})}
/>
{errors.email && (
<span className='text-red-500'>Email is required</span>
)}
<input
type='text'
placeholder='Username'
className='bg-transparent w-full py-2 px-4 rounded mt-6 border-neutral-400 border'
{...register('username', {
required: true,
minLength: 3,
maxLength: 20,
})}
/>
{errors.username && (
<span className='text-red-500'>Username is required</span>
)}
<input
type='password'
placeholder='Password'
className='bg-transparent w-full py-2 px-4 rounded mt-6 border-neutral-400 border'
{...register('password', {
required: true,
minLength: 3,
maxLength: 20,
})}
/>
{errors.password && (
<span className='text-red-500'>Password is required</span>
)}
{children}
<button
type='submit'
className='bg-yellow-500 uppercase font-bold mt-6 rounded-lg p-2 text-neutral-800 hover:bg-yellow-600'
>
Daftar
</button>
</div>
</div>
</form>
</>
);
};

View File

@ -0,0 +1,40 @@
export const Kebutuhan = () => {
return (
<>
<div className="w-full p-8 bg-white border border-gray-200 rounded-lg shadow-md">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-blue-400">Kebutuhan (C2)</h2>
<button className="items-end bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600 mb-2">Tambah Data</button>
</div>
<div className="flex justify-center">
<table className="border border-collapse border-gray-500 w-full">
<thead>
<tr className="bg-yellow-300 text-black">
<th className="border border-gray-500">
No
</th>
<th className="border border-gray-500">
Nama Sub-kriteria
</th>
<th className="border border-gray-500">
Nilai
</th>
<th className="border border-gray-500">
Aksi
</th>
</tr>
</thead>
<tbody className="text-center">
<tr>
<td className="border border-gray-500">1.</td>
<td className="border border-gray-500">Rp.1000</td>
<td className="border border-gray-500">1000</td>
<td className="border border-gray-500">-</td>
</tr>
</tbody>
</table>
</div>
</div>
</>
);
}

View File

@ -0,0 +1,38 @@
export const Kualitas = () => {
return (
<div className="w-full p-8 bg-white border border-gray-200 rounded-lg shadow-md">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-blue-400">Kualitas (C3)</h2>
<button className="items-end bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600 mb-2">Tambah Data</button>
</div>
<div className="flex justify-center">
<table className="border border-collapse border-gray-500 w-full">
<thead>
<tr className="bg-yellow-300 text-black">
<th className="border border-gray-500">
No
</th>
<th className="border border-gray-500">
Nama Sub-kriteria
</th>
<th className="border border-gray-500">
Nilai
</th>
<th className="border border-gray-500">
Aksi
</th>
</tr>
</thead>
<tbody className="text-center">
<tr>
<td className="border border-gray-500">1.</td>
<td className="border border-gray-500">Rp.1000</td>
<td className="border border-gray-500">1000</td>
<td className="border border-gray-500">-</td>
</tr>
</tbody>
</table>
</div>
</div>
);
}

View File

@ -0,0 +1,8 @@
export const Spinner = () => {
return (
<svg aria-hidden="true" role="status" className="inline w-4 h-4 me-3 text-white animate-spin" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="#E5E7EB" />
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentColor" />
</svg>
)
}

View File

@ -0,0 +1,23 @@
import { useLogin } from "../hooks/useLogin";
interface NavbarProps {
setCurrentPage: (page: string) => void;
}
export const Navbar: React.FC<NavbarProps> = ({ setCurrentPage }) => {
const { isAuthenticated } = useLogin()
return (
<div>
<nav className='flex justify-between items-center p-5 text-white bg-black bg-opacity-50 w-full absolute z-20'>
<h1 className='text-end text-2xl font-bold z-20'>Sistem Pendukung Keputusan</h1>
<ul className='flex justify-start items-start space-x-5'>
<li className='cursor-pointer text-white hover:underline hover:text-gray-500 z-20' onClick={() => setCurrentPage('about')}>About Us</li>
<li className='text-white z-20'>|</li>
<li className='cursor-pointer text-white hover:underline hover:text-gray-500 z-20' onClick={() => setCurrentPage('dashboard')}>{isAuthenticated ? 'Dashboard' : 'Login'}</li>
<li className='text-white z-20'>|</li>
<li className='cursor-pointer text-white hover:underline hover:text-gray-500 z-20' onClick={() => setCurrentPage('home')}>Home</li>
</ul>
</nav>
</div>
)
}

View File

@ -0,0 +1,41 @@
import { useGetUtility } from "../hooks/perhitungan/useGetUtility";
export const NilaiUtility = () => {
const { data, isLoading, isError } = useGetUtility();
if (isLoading) return <div className="text-center">Loading...</div>;
if (isError) return <div className="text-center">Error loading data</div>;
return (
<>
{data?.matrixPerJenis.map((group, idx) => (
<div key={idx} className="w-full p-8 bg-white border border-gray-200 rounded-lg shadow-md mb-6">
<h2 className="text-lg font-semibold text-blue-400 mb-4">Nilai Utility - {group.jenis}</h2>
<div className="max-h-96 overflow-x-auto">
<table className="border border-collapse border-gray-500 w-full">
<thead>
<tr className="bg-yellow-300 text-black">
<th className="border border-gray-500">No</th>
<th className="border border-gray-500">Alternatif</th>
{group.dataHeader.map((header, index) => (
<th key={index} className="border border-gray-500">{header}</th>
))}
</tr>
</thead>
<tbody className="text-center">
{group.dataTable.map((item, index) => (
<tr key={index}>
<td className="border border-gray-500">{index + 1}</td>
<td className="border border-gray-500">{item.alternatif}</td>
{item.criteria.map((c, ci) => (
<td key={ci} className="border border-gray-500">{c.nilaiUtility.toFixed(2)}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
))}
</>
);
};

View File

@ -0,0 +1,37 @@
import { useGetNormalisasiBobot } from "../hooks/perhitungan/useGetNormalisasiBobot";
export const NormalisasiBobot = () => {
const { data, isLoading, isError } = useGetNormalisasiBobot();
if (isLoading) return <div className="text-center">Loading...</div>;
if (isError) return <div className="text-center">Error loading data</div>;
return (
<div className="w-full p-8 bg-white border border-gray-200 rounded-lg shadow-md">
<div className="flex items-center justify-between mb-2">
<h2 className="text-lg font-semibold text-blue-400">Normalisasi Bobot Kriteria</h2>
{/* <button className="items-end bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600 mb-2">Tambah Data</button> */}
</div>
<div className="flex justify-center">
<table className="border border-collapse border-gray-500 w-full">
<thead>
<tr className="bg-yellow-300 text-black">
{data?.map((item, index) => (
<th key={index} className="border border-gray-500">
{item.code}
</th>
))}
</tr>
</thead>
<tbody className="text-center">
<tr>
{data?.map((item, index) => (
<td key={index} className="border border-gray-500">
{item.weight}
</td>
))}
</tr>
</tbody>
</table>
</div>
</div>
);
}

View File

@ -0,0 +1,37 @@
import { useGetTotal } from "../hooks/perhitungan/useGetTotal";
export const PerhitunganNilai = () => {
const { data, isLoading, isError } = useGetTotal();
if (isLoading) return <div className="text-center">Loading...</div>;
if (isError) return <div className="text-center">Error loading data</div>;
return (
<div className="w-full p-8 bg-white border border-gray-200 rounded-lg shadow-md">
<h2 className="text-lg font-semibold text-blue-400 mb-4">Perhitungan Total Nilai Setiap Jenis Alat Musik</h2>
{data?.matrixTotal.map((group, idx) => (
<div key={idx} className="mb-8">
<h3 className="text-md font-semibold text-gray-700 mb-2">{group.jenis}</h3>
<table className="border border-collapse border-gray-500 w-full mb-4">
<thead>
<tr className="bg-yellow-300 text-black">
<th className="border border-gray-500">No</th>
<th className="border border-gray-500">Alternatif</th>
<th className="border border-gray-500">Total Nilai</th>
</tr>
</thead>
<tbody className="text-center">
{group.data.map((item, i) => (
<tr key={i}>
<td className="border border-gray-500">{i + 1}</td>
<td className="border border-gray-500">{item.alternatif}</td>
<td className="border border-gray-500">{item.totalNilai.toFixed(2)}</td>
</tr>
))}
</tbody>
</table>
</div>
))}
</div>
);
};

View File

@ -0,0 +1,120 @@
import React, { useMemo, useState } from "react";
import { SiAlwaysdata } from "react-icons/si";
import { BsClipboard2DataFill, BsFilePerson } from "react-icons/bs";
import { GrMultiple } from "react-icons/gr";
import { GiGuitar } from "react-icons/gi";
import { IoIosCalculator } from "react-icons/io";
import { SiVirustotal } from "react-icons/si";
import { AiOutlineMenu } from "react-icons/ai";
import { useLogin } from "../hooks/useLogin";
import { BiLogOut } from "react-icons/bi";
import { useGetPhotoProfile } from "../hooks/profile/useGetPhotoProfile";
// import { set } from "react-hook-form";
interface sidebarProps {
setGim: (page: string) => void;
}
export const SideBar: React.FC<sidebarProps> = ({ setGim }) => {
const [expand, setExpand] = useState(true);
const [active, setActive] = useState<number | null>(null);
const { userData } = useLogin()
const { me } = useLogin()
const { data: dataProfile, refetch } = useGetPhotoProfile()
refetch()
const toggleSideBar = () => {
setExpand(!expand);
}
const useHandle = (index: number, page: string) => {
setActive(index);
setGim(page);
}
const menu = [
{ icon: BsClipboard2DataFill, label: "Data Kriteria", page: "kriteria" },
{ icon: GiGuitar, label: "Jenis Alat Musik", page: "jenisAlatMusik" },
{ icon: GrMultiple, label: "Data Sub-kriteria & Alternatif", page: "subkriteria" },
// { icon: TbChartInfographic, label: "Data Penilaian", page: "penilaian" },
{ icon: IoIosCalculator, label: "Data Perhitungan", page: "perhitungan" },
{ icon: SiVirustotal, label: "Data Hasil Akhir", page: "hasil" },
{ icon: BsFilePerson, label: "Profile", page: "profile" },
];
const menuItems = useMemo(() => {
const role = userData?.data?.role;
return menu.filter((item) => {
const page = item.page.toLowerCase();
if (role === "ADMIN") {
// Admin bisa akses semua kecuali subkriteria
return page !== "subkriteria";
}
// User biasa hanya bisa akses hasil, profile, dan subkriteria
return ["hasil", "profile", "subkriteria"].includes(page);
});
}, [userData]);
const handleLogout = () => {
localStorage.removeItem('token')
me();
}
return (
<div className={`h-screen ${expand ? "w-64" : "w-20"} bg-black text-white flex flex-col font-livvic transition-all duration-300`}>
{/* <div className="flex flex-row justify-between py-4 px-4"> */}
<div className="flex flex-row justify-between py-4 border-b border-gray-700 px-4">
<div className="flex items-start justify-center">
<SiAlwaysdata className="text-4xl text-yellow-500" />
{expand && <h1 className="text-2xl font-bold font-livvic mx-2">SPK</h1>}
</div>
<div className="flex justify-center">
<button onClick={toggleSideBar} className={`absolute rounded-full bg-gray-800 p-2 ${expand ? "left-[235px]" : "left-[60px]"} text-white hover:text-gray-400 focus:outline-none duration-300`}>
<AiOutlineMenu className="text-2xl" />
</button>
{/* </div> */}
</div>
</div>
<nav className="flex-1 mt-4">
<ul className="space-y-4 px-2">
{menuItems.map((item, index) => (
<li key={index}>
<a className={`flex items-center ${expand ? '' : 'justify-center'} space-x-2 p-2 rounded-full duration-300 cursor-pointer ${active === index ? "bg-white text-black" : "hover:bg-gray-800"}`} onClick={() => useHandle(index, item.page)}>
<item.icon className="text-xl" />
{expand && <span>{item.label}</span>}
</a>
</li>
))}
</ul>
</nav>
{/* Garis pemisah di atas profil */}
<div className="border-t border-gray-700 my-4 mx-4"></div>
<div className="flex flex-col items-center px-4 mb-2 py-2">
<img
src={dataProfile?.photo ?? "/default.jpg"}
alt="Profile"
className="w-16 h-16 rounded-full border-2 border-white"
/>
{expand && (
<>
<p className="mt-2 font-semibold">{userData?.data?.username}</p>
<p className="text-sm text-gray-400">{userData?.data?.email}</p>
</>
)}
</div>
<button type="button" className={`focus:outline-none ${expand ? '' : 'justify-center'} text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm mx-2 px-5 py-2.5 me-2 mb-2 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900`} onClick={handleLogout}>
<div className="flex justify-center items-center gap-1">
<BiLogOut />
Logout
</div>
</button>
</div>
)
}

View File

@ -0,0 +1,60 @@
import { FaPlus } from "react-icons/fa6";
import { useForm } from "react-hook-form";
import { Jenis, useGetJenisQuery } from "../hooks/jenis/useGetJenisQuery";
import { useCreateJenisMutation } from "../hooks/jenis/useCreateJenisMutation";
interface tambahDataJenisProps {
onClick: () => void;
refetch?: () => void;
}
export const TambahDataJenis: React.FC<tambahDataJenisProps> = ({ onClick }) => {
const { register, handleSubmit, reset, formState: { errors } } = useForm<Jenis>();
const { mutateAsync } = useCreateJenisMutation();
const { refetch } = useGetJenisQuery();
const onSubmit = async (data: Jenis) => {
try {
await mutateAsync(data);
reset();
refetch();
onClick();
} catch (error) {
console.error("Error creating jenis:", error);
}
};
return (
<>
<div className="w-full px-6">
<div className="w-full p-8 bg-white border border-gray-200 rounded-lg shadow-md">
<div className="flex justify-between items-center mb-4 border-b">
<h2 className="text-xl font-semibold text-gray-700">Data Jenis Alat Musik</h2>
<button className="bg-gray-300 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-400 mb-2" onClick={onClick}>
Kembali
</button>
</div>
<p className="text-blue-600 font-semibold mb-4 flex items-center">
<span className="mr-2"><FaPlus /></span> Tambah Data Jenis Alat Musik
</p>
{/* Form dengan tata letak horizontal */}
<form className="w-full" onSubmit={handleSubmit(onSubmit)}>
<div className="mb-4">
<label className="block mb-1 text-sm font-medium text-gray-700">Nama Jenis Alat Musik</label>
<input type="text" {...register("nama", { required: "data tidak boleh kosong" })} className="w-full border border-gray-500 rounded-lg p-3" />
{errors.nama && <span className="text-red-500">{errors.nama.message}</span>}
</div>
<div className="flex justify-end space-x-4">
<button type="submit" className="bg-green-500 px-6 py-3 text-white hover:bg-green-600 rounded-lg flex items-center">
<span>Simpan</span>
</button>
<button type="reset" className="bg-blue-500 px-6 py-3 text-white hover:bg-blue-600 rounded-lg flex items-center" onClick={() => reset()}>
<span>Reset</span>
</button>
</div>
</form>
</div>
</div>
</>
);
};

View File

@ -0,0 +1,88 @@
import { SubmitHandler, useForm } from "react-hook-form";
import { FaPlus } from "react-icons/fa6";
import { MdSaveAlt } from "react-icons/md";
import { BiReset } from "react-icons/bi";
import { useCreateCriteriaMutation } from "../hooks/criteria/useCreateCriteriaMutation";
import { Criteria, useGetCriteriaQuery } from "../hooks/criteria/useGetCriteriaQuery";
interface FormKriteriaProps {
onClick: () => void;
}
export const TambahDataKri: React.FC<FormKriteriaProps> = ({ onClick }) => {
const { register, handleSubmit, reset, formState: { errors }, } = useForm<Criteria>();
const { mutateAsync } = useCreateCriteriaMutation();
const { refetch } = useGetCriteriaQuery();
const onSubmit: SubmitHandler<Criteria> = async (data) => {
try {
await mutateAsync({ ...data, weight: Number(data.weight) }); // weight is a number
reset();
refetch();
onClick();
} catch (error) {
console.log(error);
}
}
return (
<div className="w-full p-6">
<div className="w-full p-8 bg-white border border-gray-200 rounded-lg shadow-md">
<div className="flex justify-between items-center mb-4 border-b">
<h2 className="text-xl font-semibold text-gray-700">Data Kriteria</h2>
<button className="bg-gray-300 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-400 mb-2" onClick={onClick}>
Kembali
</button>
</div>
<p className="text-blue-600 font-semibold mb-4 flex items-center">
<span className="mr-2"><FaPlus /></span> Tambah Data Kriteria
</p>
{/* Form dengan tata letak horizontal */}
<form className="w-full" onSubmit={handleSubmit(onSubmit)}>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
{/* Kode Kriteria */}
<div className="flex-1 min-w-[200px]">
<label className="block mb-1 text-sm font-medium text-gray-700">Kode Kriteria</label>
<input type="text" {...register("code", { required: "Kode tidak boleh kosong!" })} className="w-full p-3 border border-gray-500 rounded-lg focus:ring-blue-500 focus:border-blue-500" placeholder="Kode" />
{errors.code && <span className="text-red-500">{errors.code.message}</span>}
</div>
{/* Nama Kriteria */}
<div className="flex-1 min-w-[200px]">
<label className="block mb-1 text-sm font-medium text-gray-700">Nama Kriteria</label>
<input type="text" {...register("name", { required: "Nama tidak boleh kosong!" })} className="w-full p-3 border border-gray-500 rounded-lg focus:ring-blue-500 focus:border-blue-500" placeholder="Status" />
{errors.name && <span className="text-red-500">{errors.name.message}</span>}
</div>
{/* Bobot Kriteria */}
<div className="flex-1 min-w-[200px]">
<label className="block mb-1 text-sm font-medium text-gray-700">Bobot Kriteria</label>
<input type="number" {...register("weight")} className="w-full p-3 border border-gray-500 rounded-lg focus:ring-blue-500 focus:border-blue-500" placeholder="Masukkan bobot kriteria" />
{errors.weight && <span className="text-red-500">Bobot tidak boleh kosong!</span>}
</div>
{/* Jenis Kriteria */}
<div className="flex-1 min-w-[200px]">
<label className="block mb-1 text-sm font-medium text-gray-700">Jenis Kriteria</label>
<select {...register("criteria", { required: "Criteria wajib diisi!" })} className="w-full p-3 border border-gray-500 rounded-lg focus:ring-blue-500 focus:border-blue-500">
<option value="">--Pilih Jenis Kriteria--</option>
<option value="BENEFIT">Benefit</option>
<option value="COST">Cost</option>
</select>
</div>
</div>
{/* Tombol Simpan & Reset */}
<div className="flex justify-end space-x-4">
<button type="submit" className="bg-green-500 text-white px-6 py-3 rounded-lg hover:bg-green-600 flex items-center">
<MdSaveAlt className="mr-1" /> <span>Simpan</span>
</button>
<button type="reset" className="bg-blue-500 text-white px-6 py-3 rounded-lg hover:bg-blue-600 flex items-center" onClick={() => reset()}>
<BiReset className="mr-1" /> <span>Reset</span>
</button>
</div>
</form>
</div>
</div>
);
};

View File

@ -0,0 +1,205 @@
import { BiReset } from "react-icons/bi";
import { FaPlus } from "react-icons/fa6";
import { MdSaveAlt } from "react-icons/md";
import { useGetCriteriaQuery } from "../hooks/criteria/useGetCriteriaQuery";
import { useCreateSubCriteriaMutation } from "../hooks/subKriteria/useCreateSubCriteriaMutation";
import { subCriteria } from "../hooks/subKriteria/useGetSubCriteriaMutation";
import { SubmitHandler, useForm, useFieldArray } from "react-hook-form";
import { useGetJenisQuery } from "../hooks/jenis/useGetJenisQuery";
interface TambahDataSubKriteriaProps {
onClick: () => void;
}
interface FormValues {
alternatif: string;
namaId: string;
subKriteria: subCriteria[];
}
export const TambahDataSubKriteria: React.FC<TambahDataSubKriteriaProps> = ({ onClick }) => {
const { data } = useGetCriteriaQuery();
const { mutateAsync } = useCreateSubCriteriaMutation();
const { refetch } = useGetCriteriaQuery();
const { data: jenisData } = useGetJenisQuery();
const { register, handleSubmit, reset, control, formState: { errors } } = useForm<FormValues>({
defaultValues: {
subKriteria: [{ codeId: "", nilai: 0 }]
}
});
const { fields, append, remove } = useFieldArray({
control,
name: "subKriteria"
});
const onSubmit: SubmitHandler<FormValues> = async (formData) => {
console.log(formData);
try {
for (const subKriteria of formData.subKriteria) {
await mutateAsync({ ...subKriteria, alternatif: formData.alternatif, nilai: subKriteria.nilai, namaId: formData.namaId });
}
reset();
refetch();
} catch (error) {
console.log("Error creating item:", error);
}
};
return (
<div className="w-full p-6 max-h-screen overflow-y-auto">
<div className="w-full p-8 bg-white border border-gray-200 rounded-lg shadow-md">
<div className="flex justify-between items-center mb-4 border-b">
<h2 className="text-xl font-semibold text-gray-700">Data Sub-Kriteria & Alternatif</h2>
<button className="bg-gray-300 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-400 mb-2" onClick={onClick}>
Kembali
</button>
</div>
<p className="text-blue-600 font-semibold mb-4 flex items-center">
<span className="mr-2"><FaPlus /></span> Tambah Data Sub-Kriteria & Alternatif
</p>
{/* Form dengan tata letak horizontal */}
<form className="w-full" onSubmit={handleSubmit(onSubmit)}>
<div className="grid grid-cols-1 gap-3 mb-3">
{/* Nama Alternatif */}
<div className="flex-1 min-w-[200px]">
<label className="block mb-1 text-sm font-medium text-gray-700">Nama Alternatif</label>
<input
type="text"
className="w-full p-3 border border-gray-500 rounded-lg focus:ring-blue-500 focus:border-blue-500"
placeholder="Nama Alternatif"
{...register("alternatif", { required: "Nama Alternatif is required" })}
/>
{errors.alternatif && <span className="text-red-500">{errors.alternatif.message}</span>}
</div>
{/* Jenis Alat Musik */}
<div className="flex-1 min-w-[200px]">
<label className="block mb-1 text-sm font-medium text-gray-700">Jenis Alat Musik</label>
<select className="w-full p-3 border border-gray-500 rounded-lg focus:ring-blue-500 focus:border-blue-500" {...register("namaId", { required: "Jenis wajib dipilih" })}>
<option value="">-- Pilih Jenis --</option>
{jenisData?.map(jenis => (
<option key={jenis.id} value={jenis.nama}>{jenis.nama}</option>
))}
</select>
{errors.namaId && <span className="text-red-500">{errors.namaId.message}</span>}
</div>
{fields.map((field, index) => (
<div key={field.id} className="grid grid-cols-1 gap-3 mb-3">
{/* Code Kriteria */}
<div className="flex-1 min-w-[200px]">
<label className="block mb-1 text-sm font-medium text-gray-700">Kode Criteria</label>
<select
className="w-full p-3 border border-gray-500 rounded-lg focus:ring-blue-500 focus:border-blue-500"
{...register(`subKriteria.${index}.codeId`, { required: "Kode Criteria is required" })}
>
<option value="" className="text-gray-500">-Select Code-</option>
{data?.map(e => (<option key={e.id} value={e.code}>{e.code}</option>))}
</select>
{errors.subKriteria?.[index]?.codeId && <span className="text-red-500">{errors.subKriteria[index].codeId.message}</span>}
</div>
{/* Nilai Kriteria */}
<div className="flex-1 min-w-[200px]">
<label className="block mb-1 text-sm font-medium text-gray-700">Nilai</label>
<input
type="number"
className="w-full p-3 border border-gray-500 rounded-lg focus:ring-blue-500 focus:border-blue-500"
placeholder="Masukkan bobot kriteria"
{...register(`subKriteria.${index}.nilai`, { required: "Nilai is required" })}
/>
{errors.subKriteria?.[index]?.nilai && <span className="text-red-500">{errors.subKriteria[index].nilai.message}</span>}
</div>
<button type="button" className="bg-red-500 text-white px-4 py-2 rounded-lg hover:bg-red-600" onClick={() => remove(index)}>
Hapus
</button>
</div>
))}
<button type="button" className="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600" onClick={() => append({
codeId: "", nilai: 0, alternatif: "", id: "", subKriteria: [{ codeId: "", nilai: 0 }], jenis: { nama: "" }, namaId: ""
})}>
Tambah Kriteria
</button>
</div>
{/* Tombol Simpan & Reset */}
<div className="flex justify-end space-x-4">
<button type="submit" className="bg-green-500 text-white px-6 py-3 rounded-lg hover:bg-green-600 flex items-center">
<MdSaveAlt className="mr-1" /> <span>Simpan</span>
</button>
<button type="button" className="bg-blue-500 text-white px-6 py-3 rounded-lg hover:bg-blue-600 flex items-center" onClick={() => reset()}>
<BiReset className="mr-1" /> <span>Reset</span>
</button>
</div>
<div className="mt-3">
<div className="text-sm text-gray-500 font-semibold">
<span className="text-red-500">*</span> Keterangan
</div>
<div className="text-sm text-gray-500">
<h3>Kriteria Harga Guitar</h3>
<p>100 = &lt; 2.000.000,
75 = 2.000.000 - 2.999.999,
50 = 3.000.000 - 3.999.999,
25 = &gt; 4.000.000
</p><br />
<h3>Kriteria Harga Drum</h3>
<p>
100 = &lt; 8.000.000,
75 = 8.000.000 - 8.999.999,
50 = 9.000.000 - 9.999.999,
25 = &gt; 10.000.000
</p><br />
<h3>Kriteria Harga Keyboard</h3>
<p>
100 = &lt; 6.000.000,
75 = 6.000.000 - 6.999.999,
50 = 7.000.000 - 7.999.999,
25 = &gt; 8.000.000
</p><br />
<h3>Kriteria Harga Bass</h3>
<p>
100 = &lt; 3.000.000,
75 = 3.000.000 - 3.999.999,
50 = 4.000.000 - 4.999.999,
25 = &gt; 5.000.000
</p><br />
<h3>Kriteria Harga Biola</h3>
<p>
100 = &lt; 1.000.000,
75 = 1.000.000 - 1.999.999,
50 = 2.000.000 - 2.999.999,
25 = &gt; 3.000.000
</p>
<br />
<h3>Kriteria Kebutuhan</h3>
<p>25 = Jazz,
50 = Blues,
75 = Rock,
100 = Pop
</p>
<br />
<h3>Kriteria Kualitas</h3>
<p>25 = Kurang Bagus,
50 = Biasa,
75 = Bagus,
100 = Sangat Bagus
</p>
<br />
<h3>Kriteria Visual</h3>
<p>25 = Kurang Menarik,
50 = Biasa,
75 = Menarik,
100 = Sangat Menarik
</p>
</div>
</div>
</form>
</div>
</div>
);
};

View File

@ -0,0 +1,38 @@
export const Visual = () => {
return (
<div className="w-full p-8 bg-white border border-gray-200 rounded-lg shadow-md">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-blue-400">Visual (C4)</h2>
<button className="items-end bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600 mb-2">Tambah Data</button>
</div>
<div className="flex justify-center">
<table className="border border-collapse border-gray-500 w-full">
<thead>
<tr className="bg-yellow-300 text-black">
<th className="border border-gray-500">
No
</th>
<th className="border border-gray-500">
Nama Sub-kriteria
</th>
<th className="border border-gray-500">
Nilai
</th>
<th className="border border-gray-500">
Aksi
</th>
</tr>
</thead>
<tbody className="text-center">
<tr>
<td className="border border-gray-500">1.</td>
<td className="border border-gray-500">Rp.1000</td>
<td className="border border-gray-500">1000</td>
<td className="border border-gray-500">-</td>
</tr>
</tbody>
</table>
</div>
</div>
);
}

View File

@ -0,0 +1,49 @@
// import React, { useState } from "react"
import { WithAuth } from "../../hoc/withAuth"
import { SideBar } from "../SideBar"
import { Kriteria } from "./Kriteria"
import { DataSubKriteria } from "./DataSubKriteria"
// import { TambahDataAl } from "../TambahDataAl"
import { JenisAlatMusik } from "./JenisAlatMusik"
import { Penilaian } from "./Penilaian"
import { Perhitungan } from "./Perhitungan"
import { HasilAkhir } from "./HasilAkhir"
import { Profile } from "./Profile"
import { useDashboardPage } from "../../hooks/useDashboardPage"
const Dashboard = () => {
const { page, setPage } = useDashboardPage()
return (
<>
<div className="flex h-screen">
<SideBar setGim={setPage} />
<div className="flex flex-1 justify-center items-center">
{page === 'kriteria' && <Kriteria />}
{page === 'jenisAlatMusik' && <JenisAlatMusik />}
{page === 'subkriteria' && <DataSubKriteria />}
{page === 'penilaian' && <Penilaian />}
{page === 'perhitungan' && <Perhitungan />}
{page === 'hasil' && <HasilAkhir />}
{page === 'profile' && <Profile />}
{page === 'dashboard' && (
<div className="text-center max-w-lg">
<p className="mb-4 font-livvic text-xl">
Selamat datang di dalam sistem pendukung keputusan pemilihan alat musik yang dapat membantu dalam mengambil setiap keputusan dengan baik
</p>
<img
src="assets/undraw-code.png"
alt="Kriteria"
className="h-auto max-w-full mx-auto"
/>
</div>
)}
</div>
</div>
</>
)
}
export default WithAuth(Dashboard, { activate: true })

View File

@ -0,0 +1,160 @@
import { useMemo, useState } from "react";
import { TambahDataSubKriteria } from "../TambahDataSubKriteria";
import { subCriteria, useGetSubCriteriaQuery } from "../../hooks/subKriteria/useGetSubCriteriaMutation";
// import { Alternatif } from "./Alternatif";
import { useGetCriteriaQuery } from "../../hooks/criteria/useGetCriteriaQuery";
import { useDeleteSubCriteriaMutation } from "../../hooks/subKriteria/useDeleteSubCriteriaMutation";
import { EditSubCriteria } from "../EditSubCriteria";
import { FormEditSubCriteria } from "../FormEditSubCriteria";
// import { it } from "node:test";
// import { string } from "joi";
interface EditSubCriteria {
[key: string]: { value: number; id: string; }
}
export const DataSubKriteria = () => {
const [list, setlist] = useState(true);
const { data, refetch } = useGetSubCriteriaQuery()
const { data: criteriaData } = useGetCriteriaQuery();
const { mutateAsync } = useDeleteSubCriteriaMutation()
const handleDelete = async (alternatifName: string) => {
const subCriteriaId = data?.filter(item => item.alternatif === alternatifName).map(item => item.id);
if (subCriteriaId && subCriteriaId.length > 0) {
await Promise.all(subCriteriaId.map(id => mutateAsync(id)));
refetch();
} else {
console.error("Sub Criteria not found");
}
}
const groupDataByAlternatif = (data: subCriteria[]) => {
// i need group all sub criteria by alternatif name
const groupedCriteriaByAlternatifName: { [key: string]: { [key: string]: { value: number, id: string }, } } = {};
data.forEach((item) => {
if (!groupedCriteriaByAlternatifName[item.alternatif]) {
groupedCriteriaByAlternatifName[item.alternatif] = {};
}
groupedCriteriaByAlternatifName[item.alternatif][item.codeId] = { value: item.nilai, id: item.id };
});
return groupedCriteriaByAlternatifName;
}
const groupedData = useMemo(() => groupDataByAlternatif(data || []), [data]);
const [subCriteria, setEditSubCriteria] = useState<string>('');
return (
<>
{list ? (
<div className="w-full px-6">
<EditSubCriteria onClickClose={() => setEditSubCriteria('')} isOpen={subCriteria ? true : false} title="Edit Sub-Kriteria">
<FormEditSubCriteria onClickClose={() => setEditSubCriteria('')} subKey={subCriteria} />
</EditSubCriteria>
<div className="justify-center h-screen overflow-y-auto w-full py-5">
<div className="flex flex-col gap-4">
<div className="w-full p-8 bg-white border border-gray-200 rounded-lg shadow-md mb-4">
<div className="flex items-center just mb-4 border-b">
<h2 className="text-xl font-semibold text-gray-700">Data Sub-Kriteria & Alternatif</h2>
</div>
<div className="flex justify-between items-end mb-2">
<h4 className="text-xl font-semibold text-gray-700">Alternatif Guitar</h4>
<button className="bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600 mb-2" onClick={() => setlist(false)}>Tambah Data</button>
</div>
<div className="max-h-96 overflow-y-auto">
<table className="border border-collapse border-gray-500 w-full">
<thead className="sticky top-0 bg-yellow-300 text-black">
<tr>
<th className="border border-gray-500">
No
</th>
<th className="border border-gray-500">
Nama Alternatif
</th>
<th className="border border-gray-500">
Jenis
</th>
{criteriaData?.map((item) => (
<th key={item.id} className="border border-gray-500">
{item.code}
</th>
))}
<th className="border border-gray-500">
Aksi
</th>
</tr>
</thead>
<tbody className="text-center">
{Object.keys(groupedData).map((alternatif, index) => {
return (
<tr key={index}>
<td className="border border-gray-500">{index + 1}</td>
<td className="border border-gray-500">{alternatif}</td>
<td className="border border-gray-500">
{data?.find(item => item.alternatif === alternatif)?.namaId || "-"}
</td>
{
criteriaData?.map((item, index) => (
// <td key={index} className="border border-gray-500">{groupedData[alternatif][item.code] ? groupedData[alternatif][item.code] : "-"}</td>
<td key={index} className="border border-gray-500">{groupedData[alternatif][item.code]?.value || "-"}</td>
))
}
<td className="border border-gray-500 py-2">
<button className="focus:outline-none bg-yellow-500 text-white px-4 py-2 me-2 mb-2 rounded-lg font-medium text-sm focus:ring-4 hover:bg-yellow-600 dark:focus:ring-yellow-900"
onClick={() => setEditSubCriteria(alternatif)}
>
Edit
</button>
<button className="focus:outline-none bg-red-700 text-white px-4 py-2 me-2 mb-2 rounded-lg font-medium text-sm focus:ring-4 hover:bg-red-800 dark:focus:ring-red-900" onClick={() => handleDelete(alternatif)}>
Hapus
</button>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
</div>
{/* <div className="w-full p-8 bg-white border border-gray-200 rounded-lg shadow-md">
<div className="flex items-center justify-between mb-4 border-b">
<h2 className="text-xl font-semibold text-gray-700">Data Alternatif Guitar</h2>
<button className="bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600 mb-2" onClick={() => setlist(false)}>Tambah Data</button>
</div>
</div>
<div className="w-full p-8 bg-white border border-gray-200 rounded-lg shadow-md">
<div className="flex items-center justify-between mb-4 border-b">
<h2 className="text-xl font-semibold text-gray-700">Data Alternatif Drum</h2>
<button className="bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600 mb-2" onClick={() => setlist(false)}>Tambah Data</button>
</div>
</div>
<div className="w-full p-8 bg-white border border-gray-200 rounded-lg shadow-md">
<div className="flex items-center justify-between mb-4 border-b">
<h2 className="text-xl font-semibold text-gray-700">Data Alternatif Keyboard</h2>
<button className="bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600 mb-2" onClick={() => setlist(false)}>Tambah Data</button>
</div>
</div>
<div className="w-full p-8 bg-white border border-gray-200 rounded-lg shadow-md">
<div className="flex items-center justify-between mb-4 border-b">
<h2 className="text-xl font-semibold text-gray-700">Data Alternatif Bass</h2>
<button className="bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600 mb-2" onClick={() => setlist(false)}>Tambah Data</button>
</div>
</div>
<div className="w-full p-8 bg-white border border-gray-200 rounded-lg shadow-md">
<div className="flex items-center justify-between mb-4 border-b">
<h2 className="text-xl font-semibold text-gray-700">Data Alternatif Biola</h2>
<button className="bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600 mb-2" onClick={() => setlist(false)}>Tambah Data</button>
</div>
</div> */}
</div>
</div>
</div>
) : (<TambahDataSubKriteria onClick={() => setlist(true)} />
)}
</>
);
};

View File

@ -0,0 +1,39 @@
import { useGetRanking } from "../../hooks/perhitungan/useGetRanking";
export const HasilAkhir = () => {
const { data, isLoading, isError } = useGetRanking();
if (isLoading) return <div className="text-center">Loading...</div>;
if (isError) return <div className="text-center">Error loading data</div>;
return (
<div className="w-full overflow-y-auto h-screen px-6 py-5">
{data?.map((group, i) => (
<div key={i} className="w-full p-8 bg-white border border-gray-200 rounded-lg shadow-md mb-6">
<h2 className="text-xl font-semibold text-blue-400 mb-4">
Ranking Alternatif {group.jenis}
</h2>
<table className="w-full border border-collapse border-gray-500">
<thead>
<tr className="bg-yellow-300 text-black">
<th className="border border-gray-500">No</th>
<th className="border border-gray-500">Alternatif</th>
{/* <th className="border border-gray-500">Total Nilai</th> */}
<th className="border border-gray-500">Ranking</th>
</tr>
</thead>
<tbody className="text-center">
{group.data.map((item, index) => (
<tr key={index}>
<td className="border border-gray-500">{index + 1}</td>
<td className="border border-gray-500">{item.alternatif}</td>
{/* <td className="border border-gray-500">{item.totalNilai}</td> */}
<td className="border border-gray-500">{item.ranking}</td>
</tr>
))}
</tbody>
</table>
</div>
))}
</div>
);
};

View File

@ -0,0 +1,80 @@
import { useState } from "react"
import { TambahDataJenis } from "../TambahDataJenis"
import { Jenis, useGetJenisQuery } from "../../hooks/jenis/useGetJenisQuery"
import { EditCriteria } from "../EditCriteria"
import { FormEditJenis } from "../FormEditJenis"
import { useDeleteJenisMutation } from "../../hooks/jenis/useDeleteJenisMutation"
export const JenisAlatMusik = () => {
const [edit, setEditCriteria] = useState<Jenis | null>(null);
const [list, setlist] = useState(true);
const { data, isLoading, refetch } = useGetJenisQuery()
const { mutateAsync } = useDeleteJenisMutation()
const handleDelete = async (id: string) => {
await mutateAsync(id)
refetch()
}
if (isLoading) {
return (
<div className="text-center text-gray-500 mt-10">
Loading data...
</div>
)
}
return (
<>
{list ? (
<div className="w-full px-6">
<EditCriteria onClickClose={() => setEditCriteria(null)} isOpen={edit ? true : false} title="Edit Jenis Alat Musik">
<FormEditJenis onClickClose={() => setEditCriteria(null)} data={edit!}></FormEditJenis>
</EditCriteria>
<div className="w-full p-8 bg-white border border-gray-200 rounded-lg shadow-md">
<div className="flex justify-between items-center mb-4 border-b">
<h2 className="text-xl font-semibold text-gray-700">Data Jenis Alat Musik</h2>
<button className="bg-gray-300 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-400 mb-2" onClick={() => setlist(!true)}>
Tambah Data
</button>
</div>
<div className="flex justify-center">
<table className="border border-collapse border-gray-500 w-full">
<thead>
<tr className="bg-yellow-300 text-black">
<th className="border border-gray-500">
No
</th>
<th className="border border-gray-500">
Jenis Alat Musik
</th>
<th className="border border-gray-500">
Aksi
</th>
</tr>
</thead>
<tbody className="text-center">
{data?.map((item, index) => (
<tr key={index}>
<td className="border border-gray-500">{index + 1}</td>
<td className="border border-gray-500">{item.nama}</td>
<td className="border border-gray-500 py-2">
<button className="focus:outline-none text-white bg-yellow-400 hover:bg-yellow-500 focus:ring-4 focus:ring-yellow-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:focus:ring-yellow-900" onClick={() => setEditCriteria(item)}>
{/* {isLoading || isPending && <Spinner />} */}
Edit
</button>
<button className="focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900" onClick={() => handleDelete(item.id)}>
{/* {isLoading || isPending && <Spinner />} */}
Hapus
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
) : (<TambahDataJenis onClick={() => setlist(true)} refetch={refetch} />
)}
</>
)
}

View File

@ -0,0 +1,90 @@
import { useState } from "react";
import { TambahDataKri } from "../TambahDataKri";
import { Criteria, useGetCriteriaQuery } from "../../hooks/criteria/useGetCriteriaQuery";
import { Spinner } from "../LoadingButton";
import { useCriteriaMutation } from "../../hooks/criteria/useDeleteCriteriaMutation";
import { EditCriteria } from "../EditCriteria";
import { FormEditKriteria } from "../FormEditKriteria";
export const Kriteria = () => {
const [criteria, setEditCriteria] = useState<Criteria | null>(null);
const [list, setlist] = useState(true);
const { data, isLoading, refetch } = useGetCriteriaQuery()
const { mutateAsync, isPending } = useCriteriaMutation()
const handleDelete = async (id: string) => {
await mutateAsync(id)
refetch()
}
return (
<>
{/* PopUp Section */}
{list ? (
<div className="w-full px-6">
{/* Main Section */}
<EditCriteria onClickClose={() => setEditCriteria(null)} isOpen={criteria ? true : false} title="Edit Kriteria">
<FormEditKriteria onClickClose={() => setEditCriteria(null)} data={criteria!}></FormEditKriteria>
</EditCriteria>
<div className="w-full p-8 bg-white border border-gray-200 rounded-lg shadow-md">
<div className="flex justify-between items-center mb-4 border-b">
<h2 className="text-xl font-semibold text-gray-700">Data kriteria</h2>
<button className="bg-gray-300 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-400 mb-2" onClick={() => setlist(false)}>
Tambah Data
</button>
</div>
<div className="max-h-96 overflow-y-auto">
<table className="border border-collapse border-gray-500 w-full">
<thead className="sticky top-0 bg-yellow-300 text-black">
<tr>
<th className="border border-gray-500">
No.
</th>
<th className="border border-gray-500">
Kode Kriteria
</th>
<th className="border border-gray-500">
Nama Kriteria
</th>
<th className="border border-gray-500">
Bobot
</th>
<th className="border border-gray-500">
Jenis
</th>
<th className="border border-gray-500">
Aksi
</th>
</tr>
</thead>
<tbody className="text-center">
{data?.map((item, index) => (
<tr key={index}>
<td className="border border-gray-500">{index + 1}</td>
<td className="border border-gray-500">{item.code}</td>
<td className="border border-gray-500">{item.name}</td>
<td className="border border-gray-500">{item.weight}</td>
<td className="border border-gray-500">{item.criteria}</td>
<td className="border border-gray-500 py-2">
<button className="focus:outline-none text-white bg-yellow-400 hover:bg-yellow-500 focus:ring-4 focus:ring-yellow-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:focus:ring-yellow-900" onClick={() => setEditCriteria(item)}>
{isLoading || isPending && <Spinner />}
Edit
</button>
<button className="focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900" onClick={() => handleDelete(item.id)}>
{isLoading || isPending && <Spinner />}
Hapus
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
) : (
<TambahDataKri onClick={() => setlist(true)} />
)}
</>
);
};

View File

@ -0,0 +1,37 @@
export const Penilaian = () => {
return (
<>
<div className="w-full px-6">
<div className="w-full p-8 bg-white border border-gray-200 rounded-lg shadow-md">
<div className="flex justify-between items-center mb-4 border-b">
<h2 className="text-xl font-semibold text-gray-700">Data Penilaian</h2>
</div>
<div className="flex justify-center">
<table className="border border-collapse border-gray-500 w-full">
<thead>
<tr className="bg-yellow-300 text-black">
<th className="border border-gray-500">
No
</th>
<th className="border border-gray-500">
Alternatif
</th>
<th className="border border-gray-500">
Aksi
</th>
</tr>
</thead>
<tbody className="text-center">
<tr>
<td className="border border-gray-500">1.</td>
<td className="border border-gray-500">Rp.1000</td>
<td className="border border-gray-500"><button className="bg-yellow-300 rounded-lg px-2 py-1 my-2">Edit</button></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</>
);
}

View File

@ -0,0 +1,56 @@
import { BobotKriteria } from "../BobotKriteria";
import { NilaiUtility } from "../NilaiUtility";
import { NormalisasiBobot } from "../NormalisasiBobot";
import { PerhitunganNilai } from "../PerhitunganNilai";
import { useGetDataMatrix } from "../../hooks/perhitungan/useGetDataMatrix";
export const Perhitungan = () => {
const { data, isLoading, isError } = useGetDataMatrix();
if (isLoading) return <div className="text-center">Loading...</div>;
if (isError) return <div className="text-center">Error loading data</div>;
return (
<>
<div className="w-full overflow-y-auto h-screen px-6 py-5">
<div className="flex flex-col gap-4 pb-5">
{data?.matrixPerJenis.map((group, idx) => (
<div key={idx} className="w-full p-8 bg-white border border-gray-200 rounded-lg shadow-md">
<div className="mb-4 border-b">
<h2 className="text-xl font-semibold text-blue-400">
Matrix Keputusan - {group.jenis}
</h2>
</div>
<div className="max-h-96 overflow-y-auto">
<table className="border border-collapse border-gray-500 w-full">
<thead>
<tr className="bg-yellow-300 text-black">
<th className="border border-gray-500">No</th>
<th className="border border-gray-500">Alternatif</th>
{data?.dataHeader.map((header, index) => (
<th key={index} className="border border-gray-500">{header.code}</th>
))}
</tr>
</thead>
<tbody className="text-center">
{group.dataTable.map((item, i) => (
<tr key={i}>
<td className="border border-gray-500">{i + 1}</td>
<td className="border border-gray-500">{item.alternatif}</td>
{item.criteria.map((c, ci) => (
<td key={ci} className="border border-gray-500">{c.nilai}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
))}
<BobotKriteria />
<NormalisasiBobot />
<NilaiUtility />
<PerhitunganNilai />
</div>
</div>
</>
);
}

View File

@ -0,0 +1,93 @@
import { useSendProfile } from '../../hooks/profile/useSendProfile';
import { useState } from 'react';
import { useGetPhotoProfile } from '../../hooks/profile/useGetPhotoProfile';
import { useDeletePhoto } from '../../hooks/profile/useDeletePhoto';
export const Profile = () => {
const [image, setImage] = useState<File | null>(null);
const [] = useState('');
const [fileId, setFileId] = useState('');
const { AsyncCall, isLoading: isLoadingUpload } = useSendProfile();
const [previewUrl, setPreviewUrl] = useState<string>('');
const { data: dataProfile, refetch, isLoading: isLoadingProfile } = useGetPhotoProfile()
const { mutateAsync } = useDeletePhoto();
// const { deletePhoto } = useDeletePhoto()
// console.log("dataProfile", dataProfile);
const handleImageChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
setImage(file);
const reader = new FileReader();
reader.onloadend = () => {
setPreviewUrl(reader.result as string);
};
reader.readAsDataURL(file);
}
};
// console.log("previewUrl", previewUrl);
// console.log("data", data);
const handleUpload = async () => {
if (image) {
let format = null;
const base64 = await new Promise<string>((resolve, reject) => {
const reader = new FileReader();
format = image.name.split('.').pop() as string;
reader.readAsDataURL(image);
reader.onload = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error);
});
if (!format) return
await AsyncCall(base64, dataProfile?.id + (new Date).toDateString(), format);
setFileId(fileId);
refetch();
}
};
const handleDelete = async () => {
if (dataProfile?.photo) {
await mutateAsync()
refetch()
// try {
// await mutateAsync(dataProfile?.photo as string)
// refetch()
// } catch (error) {
// console.error("Error deleting photo:", error);
// }
}
}
return (
<div className="p-6 border rounded mx-auto max-w-md text-center shadow-lg">
<h2 className="text-xl font-semibold mb-4">{dataProfile?.name}</h2>
<div className="justify-items-center">
{isLoadingUpload || isLoadingProfile ? <p>Loading...</p> : (
<img src={previewUrl ? previewUrl : dataProfile?.photo ?? ''} alt={dataProfile?.name}
className="w-48 h-48 rounded-full border-4 border-gray-400 object-cover mx-auto mb-4" />
)}
<input type="file" accept='image' onChange={(e) => {
setImage(e.target.files?.[0] || null)
handleImageChange(e)
}} />
{/* <button onClick={() => setPreviewUrl(URL.createObjectURL(image!))} className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
Preview
</button> */}
</div>
<div className="mt-3 mb-2" aria-disabled={true}>
<div className="font-semibold">{dataProfile?.username}</div>
<div className="text-gray-500">{dataProfile?.email}</div>
</div>
<div className="flex items-center justify-center space-x-4">
<button onClick={handleUpload} className="bg-blue-500 px-2 my-3 rounded py-1 text-white hover:bg-blue-600">Upload</button>
<button onClick={handleDelete} className=" flex bg-red-500 px-2 my-3 rounded py-1 text-white hover:bg-red-600">Delete</button>
</div>
</div>
);
};

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More