Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
PORT=3000
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=
DB_NAME=tbb5_pweb
DB_PORT=3306
SESSION_SECRET=facultyware_secret
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,8 @@ typings/

# next.js build output
.next

# Playwright
playwright-report/
test-results/

139 changes: 139 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# FacultyWare — Sistem Perjalanan Dinas Fakultas

FacultyWare adalah aplikasi web berbasis Node.js, Express.js, MySQL, dan EJS yang dirancang untuk mengelola permohonan perjalanan dinas fakultas secara efisien dan transparan. Aplikasi ini memiliki alur kerja lengkap mulai dari pengajuan perjalanan dinas, manajemen jadwal (itinerary), anggota perjalanan, unggah berkas surat tugas, penulisan laporan hasil perjalanan (outcome), hingga pengajuan reimburse biaya perjalanan dinas.

---

## 1. Tech Stack

* **Backend**: Node.js & Express.js
* **Database**: MySQL (menggunakan query SQL mentah melalui driver `mysql2` tanpa ORM)
* **View Engine**: EJS (Embedded JavaScript templates)
* **Styling**: Tailwind CSS & Basecoat UI Library
* **Interaktivitas**: Vanilla JavaScript & HTMX (untuk pembaruan parsial dinamis)
* **Autentikasi & Sesi**: `express-session` & `express-mysql-session`
* **Keamanan**: Enkripsi password menggunakan `bcryptjs`
* **Unggah Berkas**: `multer`
* **Ekspor Dokumen**: PDFKit (PDF) & SheetJS/xlsx (Excel)

---

## 2. Cara Instalasi & Konfigurasi

### Langkah 1: Kloning & Install Dependencies
Pastikan Node.js dan npm telah terinstal di komputer Anda. Jalankan perintah berikut di terminal:
```bash
npm install
```

### Langkah 2: Konfigurasi Environment Variables (`.env`)
Salin file `.env.example` menjadi `.env` di direktori utama:
```bash
cp .env.example .env
```
Sesuaikan konfigurasi database sesuai dengan server MySQL lokal Anda:
```env
PORT=3000
DB_HOST=127.0.0.1
DB_USER=root
DB_PASSWORD=
DB_NAME=tb_b5
DB_PORT=3306
SESSION_SECRET=facultyware_secret
```

### Langkah 3: Inisialisasi Database
Buat database baru di server MySQL Anda dengan nama sesuai konfigurasi `.env` (misal: `tb_b5`). Impor file dump SQL database Anda ke dalamnya.

Jalankan script inisialisasi lokal untuk membuat tabel relasi pendukung perjalanan dinas jika belum tersedia:
```bash
node scripts/create_local_tables.js
```

---

## 3. Cara Menjalankan Aplikasi

### Mode Pengembangan (Development)
Untuk menjalankan server dengan auto-reload menggunakan `nodemon`:
```bash
npm run dev
```
Aplikasi akan aktif di alamat: [http://localhost:3000](http://localhost:3000).

### Mode Produksi (Production)
Untuk menjalankan aplikasi dalam mode produksi:
```bash
npm start
```

### Pengujian E2E (Playwright)
Untuk menjalankan pengetesan E2E otomatis:
```bash
# Jalankan seluruh rangkaian test suite
npx playwright test --project=chromium

# Jalankan test suite dengan UI interaktif
npx playwright test --ui
```

---

## 4. Akun Demo & Peran (Role)

Aplikasi memiliki dua jenis peran (role) utama untuk masuk ke sistem:

### Pegawai
* **Username**: `pegawai`
* **Password**: `password`
* **Fitur**: Input/CRUD perjalanan dinas, kelola itinerary/dokumen/anggota, unggah laporan hasil perjalanan, pengajuan reimburse, ekspor/impor Excel, dan unduh PDF Surat Tugas.

### Pimpinan
* **Username**: `pimpinan`
* **Password**: `password`
* **Fitur**: Dashboard pemantauan statistik, persetujuan (Approve/Reject) perjalanan dinas & klaim reimburse pegawai, peninjauan laporan, dan ekspor Excel global.

---

## Pembagian Tugas Anggota

### 1. Vanesa Gociardi

**Modul Perjalanan Dinas Pegawai**

* Pengembangan fitur pengajuan perjalanan dinas.
* Pengelolaan data perjalanan dinas pegawai.
* Menampilkan status pengajuan perjalanan.
* Pengelolaan hasil perjalanan dan rencana tindak lanjut.
* Integrasi modul pegawai dengan database.
* Deployment aplikasi dan dokumentasi proyek.

### 2. Farrah Aulia

**Modul Reimburse Perjalanan Dinas**

* Pengembangan fitur pengajuan reimburse.
* Pengelolaan komponen biaya perjalanan.
* Upload bukti pembayaran dan kwitansi.
* Pengelolaan data pengeluaran perjalanan dinas.
* Export data reimburse.

### 3. Fadel

**Modul Dokumen, Itinerary, dan Anggota Perjalanan**

* Pengelolaan itinerary perjalanan dinas.
* Pengelolaan anggota/delegasi perjalanan.
* Upload dan manajemen dokumen pendukung.
* Integrasi dokumen dengan data perjalanan dinas.
* Validasi data perjalanan.

### 4. Zaki

**Modul Persetujuan dan Dashboard Pimpinan**

* Pengembangan dashboard pimpinan.
* Persetujuan (Approve/Reject) perjalanan dinas.
* Persetujuan pengajuan reimburse.
* Monitoring seluruh data perjalanan dinas.
* Rekapitulasi dan pelaporan data perjalanan.
58 changes: 47 additions & 11 deletions app.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
require('dotenv').config();
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var session = require('express-session');
var MySQLStore = require('express-mysql-session')(session);

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const session = require('express-session');
const MySQLStore = require('express-mysql-session')(session);
const fs = require('fs');

const indexRouter = require('./routes/index');
const authRouter = require('./routes/auth');
const pegawaiRouter = require('./routes/pegawai');
const pimpinanRouter = require('./routes/pimpinan');
const apiRouter = require('./routes/api');

const { notFoundHandler, errorHandler } = require('./middlewares/error');

var app = express();
const app = express();

// Ensure upload directories exist
const uploadDocs = path.join(__dirname, 'public/uploads/documents');
const uploadReceipts = path.join(__dirname, 'public/uploads/receipts');
if (!fs.existsSync(uploadDocs)) fs.mkdirSync(uploadDocs, { recursive: true });
if (!fs.existsSync(uploadReceipts)) fs.mkdirSync(uploadReceipts, { recursive: true });

// view engine setup
app.set('views', path.join(__dirname, 'views'));
Expand All @@ -28,6 +39,7 @@ const sessionStore = new MySQLStore({
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
port: process.env.DB_PORT || 3306,
});

app.use(session({
Expand All @@ -41,8 +53,32 @@ app.use(session({
}
}));

// Session message/user variables globally exposed to EJS
app.use((req, res, next) => {
res.locals.success = req.session.success || null;
res.locals.error = req.session.error || null;
res.locals.warning = req.session.warning || null;

delete req.session.success;
delete req.session.error;
delete req.session.warning;

res.locals.user = req.session.userId ? {
id: req.session.userId,
username: req.session.username,
role: req.session.role,
employeeId: req.session.employeeId,
name: req.session.name
} : null;

next();
});

app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/', authRouter);
app.use('/pegawai', pegawaiRouter);
app.use('/pimpinan', pimpinanRouter);
app.use('/api', apiRouter);

// catch 404 and forward to error handler
app.use(notFoundHandler);
Expand Down
103 changes: 103 additions & 0 deletions controllers/apiController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
const OfficialTravel = require('../models/OfficialTravel');
const TravelItinerary = require('../models/TravelItinerary');
const TravelDocument = require('../models/TravelDocument');
const TravelMember = require('../models/TravelMember');
const TravelExpense = require('../models/TravelExpense');
const TravelApproval = require('../models/TravelApproval');

// Pegawai API Endpoints
const getMyTravel = async (req, res, next) => {
try {
const travels = await OfficialTravel.findBySubmittedBy(req.session.userId);
res.json({ success: true, data: travels });
} catch (err) {
res.status(500).json({ success: false, error: err.message });
}
};

const getMyExpenses = async (req, res, next) => {
try {
const expenses = await TravelExpense.findBySubmittedBy(req.session.employeeId || req.session.userId);
res.json({ success: true, data: expenses });
} catch (err) {
res.status(500).json({ success: false, error: err.message });
}
};

const getMyDocuments = async (req, res, next) => {
try {
const documents = await TravelDocument.findByUserId(req.session.userId);
res.json({ success: true, data: documents });
} catch (err) {
res.status(500).json({ success: false, error: err.message });
}
};

// Pimpinan API Endpoints
const getTravel = async (req, res, next) => {
try {
const travels = await OfficialTravel.findAll();
res.json({ success: true, data: travels });
} catch (err) {
res.status(500).json({ success: false, error: err.message });
}
};

const getTravelById = async (req, res, next) => {
try {
const { id } = req.params;
const travel = await OfficialTravel.findById(id);
if (!travel) {
return res.status(404).json({ success: false, error: 'Travel request not found.' });
}

const itineraries = await TravelItinerary.findByTravelId(id);
const documents = await TravelDocument.findByTravelId(id);
const members = await TravelMember.findByTravelId(id);
const expenses = await TravelExpense.findByTravelId(id);
const approvals = await TravelApproval.findByTravelId(id);

res.json({
success: true,
data: {
travel,
itineraries,
documents,
members,
expenses,
approvals
}
});
} catch (err) {
res.status(500).json({ success: false, error: err.message });
}
};

const getExpenses = async (req, res, next) => {
try {
const expenses = await TravelExpense.findAll();
res.json({ success: true, data: expenses });
} catch (err) {
res.status(500).json({ success: false, error: err.message });
}
};

const getReports = async (req, res, next) => {
try {
const travels = await OfficialTravel.findAll();
const completedTravels = travels.filter(t => t.status === 'completed');
res.json({ success: true, data: completedTravels });
} catch (err) {
res.status(500).json({ success: false, error: err.message });
}
};

module.exports = {
getMyTravel,
getMyExpenses,
getMyDocuments,
getTravel,
getTravelById,
getExpenses,
getReports
};
Loading