first commit
|
@ -0,0 +1 @@
|
||||||
|
node_modules
|
|
@ -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
|
|
@ -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?
|
|
@ -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));
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
|
@ -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
|
||||||
|
);
|
|
@ -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;
|
|
@ -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;
|
|
@ -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");
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "photo" TEXT;
|
|
@ -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");
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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"
|
|
@ -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)
|
||||||
|
}
|
|
@ -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);
|
||||||
|
})
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { Router } from "express";
|
||||||
|
import { verifyToken } from "../utils/verifyToken";
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
// const router =
|
|
@ -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;
|
|
@ -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
|
|
@ -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(),
|
||||||
|
});
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const signIn = async (email: string, password: string) => {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
export const signUp = async (email: string, password: string) => {
|
||||||
|
//
|
||||||
|
console.log('test')
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { User } from "@prisma/client";
|
||||||
|
|
||||||
|
export interface UserDTO extends User {}
|
||||||
|
|
||||||
|
export interface ResponseDTO<T> {
|
||||||
|
status: number;
|
||||||
|
message: string;
|
||||||
|
data: T;
|
||||||
|
}
|
|
@ -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}`);
|
||||||
|
});
|
||||||
|
};
|
|
@ -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();
|
||||||
|
});
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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. */
|
||||||
|
}
|
||||||
|
}
|
|
@ -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?
|
|
@ -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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
|
@ -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 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
|
@ -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>
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 2.4 MiB |
After Width: | Height: | Size: 1.8 MiB |
After Width: | Height: | Size: 3.5 MiB |
After Width: | Height: | Size: 112 KiB |
After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 68 KiB |
|
@ -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 |
After Width: | Height: | Size: 90 KiB |
|
@ -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 |
|
@ -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
|
After Width: | Height: | Size: 2.4 MiB |
After Width: | Height: | Size: 1.8 MiB |
After Width: | Height: | Size: 3.5 MiB |
After Width: | Height: | Size: 112 KiB |
After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 68 KiB |
|
@ -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 |
After Width: | Height: | Size: 90 KiB |
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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
|
||||||
|
|
||||||
|
}
|
|
@ -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 >
|
||||||
|
)
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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 = < 2.000.000,
|
||||||
|
75 = 2.000.000 - 2.999.999,
|
||||||
|
50 = 3.000.000 - 3.999.999,
|
||||||
|
25 = > 4.000.000
|
||||||
|
</p><br />
|
||||||
|
<h3>Kriteria Harga Drum</h3>
|
||||||
|
<p>
|
||||||
|
100 = < 8.000.000,
|
||||||
|
75 = 8.000.000 - 8.999.999,
|
||||||
|
50 = 9.000.000 - 9.999.999,
|
||||||
|
25 = > 10.000.000
|
||||||
|
</p><br />
|
||||||
|
<h3>Kriteria Harga Keyboard</h3>
|
||||||
|
<p>
|
||||||
|
100 = < 6.000.000,
|
||||||
|
75 = 6.000.000 - 6.999.999,
|
||||||
|
50 = 7.000.000 - 7.999.999,
|
||||||
|
25 = > 8.000.000
|
||||||
|
</p><br />
|
||||||
|
<h3>Kriteria Harga Bass</h3>
|
||||||
|
<p>
|
||||||
|
100 = < 3.000.000,
|
||||||
|
75 = 3.000.000 - 3.999.999,
|
||||||
|
50 = 4.000.000 - 4.999.999,
|
||||||
|
25 = > 5.000.000
|
||||||
|
</p><br />
|
||||||
|
<h3>Kriteria Harga Biola</h3>
|
||||||
|
<p>
|
||||||
|
100 = < 1.000.000,
|
||||||
|
75 = 1.000.000 - 1.999.999,
|
||||||
|
50 = 2.000.000 - 2.999.999,
|
||||||
|
25 = > 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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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 })
|
|
@ -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)} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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)} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|