Initial commit apps directory with .gitignore

This commit is contained in:
2026-02-22 15:15:41 +08:00
commit 0aa8cdd72c
228 changed files with 69672 additions and 0 deletions

BIN
spj-komite/._package-lock.json generated Normal file

Binary file not shown.

BIN
spj-komite/._server.js Normal file

Binary file not shown.

129
spj-komite/.gitignore vendored Normal file
View File

@@ -0,0 +1,129 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.test
.env.production
.env.local
.env.development.local
.env.test.local
.env.production.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# OS metadata
.DS_Store
Thumbs.db
# Project specific backups
*.tar.gz

Binary file not shown.

View File

@@ -0,0 +1,175 @@
-- SPJ Komite MySQL Database Schema
-- Database: db_spjkomite
-- Tabel untuk kode kegiatan
CREATE TABLE IF NOT EXISTS kode_kegiatan (
id INT AUTO_INCREMENT PRIMARY KEY,
kode VARCHAR(100) NOT NULL UNIQUE,
uraian TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Tabel untuk pengaturan/manajemen
CREATE TABLE IF NOT EXISTS pengaturan (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_ketua_komite VARCHAR(255),
bendahara VARCHAR(255),
nama_tim_teknis_a VARCHAR(255),
jabatan_tim_teknis_a VARCHAR(255),
nama_tim_teknis_b VARCHAR(255),
jabatan_tim_teknis_b VARCHAR(255),
nama_tim_teknis_c VARCHAR(255),
jabatan_tim_teknis_c VARCHAR(255),
tahun_anggaran VARCHAR(10),
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Tabel untuk RKS (Rencana Kerja dan Syarat)
CREATE TABLE IF NOT EXISTS rks (
id INT AUTO_INCREMENT PRIMARY KEY,
pekerjaan TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Counter untuk nomor dokumen
CREATE TABLE IF NOT EXISTS nomor_counter (
id INT AUTO_INCREMENT PRIMARY KEY,
tahun_anggaran VARCHAR(10) NOT NULL,
counter INT DEFAULT 0,
UNIQUE KEY unique_year (tahun_anggaran)
);
-- Tabel untuk pesanan (surat pesanan)
CREATE TABLE IF NOT EXISTS pesanan (
id INT AUTO_INCREMENT PRIMARY KEY,
tanggal_pesanan DATE NOT NULL,
nomor_pesanan VARCHAR(255) NOT NULL,
penyedia VARCHAR(255) NOT NULL,
nama_jenis_barang VARCHAR(255) NOT NULL,
spesifikasi_teknis VARCHAR(500),
volume DECIMAL(10,2) NOT NULL,
satuan VARCHAR(50) NOT NULL,
keterangan TEXT,
waktu_pengerjaan INT NOT NULL,
batas_tanggal_pengerjaan DATE NOT NULL,
penerima_pesanan VARCHAR(255),
yang_memesan VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_nomor_pesanan (nomor_pesanan)
);
-- Tabel untuk BAP (Berita Acara Pemeriksaan)
CREATE TABLE IF NOT EXISTS bap (
id INT AUTO_INCREMENT PRIMARY KEY,
nomor_bap VARCHAR(255) NOT NULL UNIQUE,
pekerjaan TEXT NOT NULL,
tanggal_pemeriksaan DATE NOT NULL,
nama_tim_teknis_a VARCHAR(255),
nama_tim_teknis_b VARCHAR(255),
nama_tim_teknis_c VARCHAR(255),
jabatan_tim_teknis_a VARCHAR(255),
jabatan_tim_teknis_b VARCHAR(255),
jabatan_tim_teknis_c VARCHAR(255),
tujuan TEXT,
alamat_penyedia TEXT,
ketua_komite VARCHAR(255),
nomor_pesanan VARCHAR(255),
kode_kegiatan VARCHAR(100),
kode_sub_kegiatan VARCHAR(100),
kode_rekening VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_nomor_bap (nomor_bap),
INDEX idx_nomor_pesanan (nomor_pesanan)
);
-- Tabel untuk BAST (Berita Acara Serah Terima)
CREATE TABLE IF NOT EXISTS bast (
id INT AUTO_INCREMENT PRIMARY KEY,
nomor_bast VARCHAR(255) NOT NULL UNIQUE,
tanggal_bast DATE NOT NULL,
nomor_bap VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_nomor_bast (nomor_bast),
INDEX idx_nomor_bap (nomor_bap)
);
-- Tabel untuk Nota
CREATE TABLE IF NOT EXISTS nota (
id INT AUTO_INCREMENT PRIMARY KEY,
nomor INT NOT NULL,
volume DECIMAL(10,2) NOT NULL,
satuan VARCHAR(50) NOT NULL,
nama_barang VARCHAR(255) NOT NULL,
harga_satuan DECIMAL(15,2) NOT NULL,
jumlah DECIMAL(15,2) NOT NULL,
nomor_bap VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_nomor_bap (nomor_bap)
);
-- Tabel untuk Foto Dokumentasi
CREATE TABLE IF NOT EXISTS foto (
id INT AUTO_INCREMENT PRIMARY KEY,
nomor_bap VARCHAR(255) NOT NULL,
related_id VARCHAR(255),
filename VARCHAR(255),
url LONGTEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_nomor_bap (nomor_bap)
);
-- Tabel untuk Aset Aplikasi (Logo dan KOP)
CREATE TABLE IF NOT EXISTS aset_aplikasi (
id INT AUTO_INCREMENT PRIMARY KEY,
tipe VARCHAR(50) NOT NULL UNIQUE,
data_url LONGTEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Insert default aset_aplikasi rows
INSERT INTO aset_aplikasi (tipe, data_url)
VALUES ('logo', NULL), ('kop', NULL)
ON DUPLICATE KEY UPDATE tipe=tipe;
-- Tabel untuk Penyedia (Supplier) dengan KOP
CREATE TABLE IF NOT EXISTS penyedia (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(255) NOT NULL UNIQUE,
alamat TEXT,
kop_url LONGTEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Tabel untuk SPJ Lomba (Kegiatan Lomba - Pembayaran Non-Tunai)
CREATE TABLE IF NOT EXISTS spj_lomba (
id INT AUTO_INCREMENT PRIMARY KEY,
nomor_spj VARCHAR(255) NOT NULL UNIQUE,
tanggal DATE NOT NULL,
nama_kegiatan VARCHAR(500) NOT NULL,
jenis_pembayaran VARCHAR(100) NOT NULL,
penerima VARCHAR(255) NOT NULL,
nama_bank VARCHAR(100) NOT NULL,
nomor_rekening VARCHAR(50) NOT NULL,
atas_nama VARCHAR(255) NOT NULL,
nominal DECIMAL(15,2) NOT NULL,
keterangan TEXT,
bukti_transfer LONGTEXT,
kode_kegiatan VARCHAR(100),
kode_sub_kegiatan VARCHAR(100),
kode_rekening VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_nomor_spj (nomor_spj),
INDEX idx_tanggal (tanggal)
);
-- Insert default pengaturan row
INSERT INTO pengaturan (id, nama_ketua_komite, bendahara, tahun_anggaran)
VALUES (1, '', '', '2026')
ON DUPLICATE KEY UPDATE id=id;
-- Insert default counter for current year
INSERT INTO nomor_counter (tahun_anggaran, counter)
VALUES ('2026', 0)
ON DUPLICATE KEY UPDATE tahun_anggaran=tahun_anggaran;

3785
spj-komite/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
spj-komite/package.json Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "spj-komite-smanab",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "npx vite",
"build": "npx vite build",
"start": "node server.js",
"preview": "npx vite preview"
},
"dependencies": {
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.19",
"dotenv": "^17.2.3",
"express": "^4.19.2",
"lucide-react": "^0.379.0",
"multer": "^2.0.2",
"mysql2": "^3.16.0",
"postcss": "^8.4.38",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwindcss": "^3.4.3",
"typescript": "^5.2.2",
"vite": "^5.2.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.12.12",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -0,0 +1,354 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - SPJ Komite SMA Negeri 1 Abiansemal</title>
<link rel="icon" type="image/png" href="/assets/logo.png">
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet">
<script src="https://unpkg.com/lucide@latest"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Nunito', 'sans-serif'],
heading: ['Poppins', 'sans-serif'],
},
colors: {
'primary-blue': '#0056b3',
'primary-blue-dark': '#004085',
'accent-green': '#28a745',
'accent-green-dark': '#218838',
'neutral-light': '#f8f9fa',
}
}
}
}
</script>
<style>
body {
font-family: 'Nunito', sans-serif;
overflow: hidden;
}
.login-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
position: fixed;
inset: 0;
}
.login-bg::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.05'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
}
.glass-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow:
0 25px 50px -12px rgba(0, 0, 0, 0.25),
0 0 0 1px rgba(255, 255, 255, 0.1) inset;
}
.floating-shapes div {
position: absolute;
border-radius: 50%;
opacity: 0.1;
animation: float 20s infinite ease-in-out;
}
.floating-shapes .shape1 {
width: 300px;
height: 300px;
background: #fff;
top: -100px;
left: -100px;
animation-delay: 0s;
}
.floating-shapes .shape2 {
width: 200px;
height: 200px;
background: #fff;
bottom: -50px;
right: -50px;
animation-delay: -5s;
}
.floating-shapes .shape3 {
width: 150px;
height: 150px;
background: #fff;
top: 50%;
right: 10%;
animation-delay: -10s;
}
@keyframes float {
0%,
100% {
transform: translateY(0) rotate(0deg);
}
50% {
transform: translateY(-30px) rotate(10deg);
}
}
.input-field {
transition: all 0.3s ease;
background: rgba(248, 249, 250, 0.8);
}
.input-field:focus {
background: white;
border-color: #667eea;
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.15);
transform: translateY(-1px);
}
.login-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.login-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s;
}
.login-btn:hover::before {
left: 100%;
}
.login-btn:hover {
transform: translateY(-3px);
box-shadow: 0 15px 30px -5px rgba(102, 126, 234, 0.5);
}
.login-btn:active {
transform: translateY(-1px);
}
.logo-container {
animation: pulse-glow 3s infinite ease-in-out;
}
@keyframes pulse-glow {
0%,
100% {
box-shadow: 0 0 20px rgba(102, 126, 234, 0.3);
}
50% {
box-shadow: 0 0 40px rgba(102, 126, 234, 0.6);
}
}
.fade-in {
animation: fadeIn 0.6s ease-out forwards;
opacity: 0;
}
.fade-in-delay-1 {
animation-delay: 0.1s;
}
.fade-in-delay-2 {
animation-delay: 0.2s;
}
.fade-in-delay-3 {
animation-delay: 0.3s;
}
.fade-in-delay-4 {
animation-delay: 0.4s;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.register-link {
color: #667eea;
transition: all 0.2s;
}
.register-link:hover {
color: #764ba2;
text-decoration: underline;
}
</style>
</head>
<body>
<div class="login-bg">
<div class="floating-shapes">
<div class="shape1"></div>
<div class="shape2"></div>
<div class="shape3"></div>
</div>
</div>
<div class="relative min-h-screen flex items-center justify-center p-4">
<div class="w-full max-w-md">
<div class="glass-card rounded-3xl p-8 md:p-10">
<!-- Logo & Header -->
<div class="text-center mb-8 fade-in fade-in-delay-1">
<div class="logo-container w-24 h-24 mx-auto mb-5 rounded-2xl bg-white p-2 shadow-lg">
<img src="/assets/logo.png" alt="Logo" class="w-full h-full object-contain">
</div>
<h1
class="font-heading text-2xl md:text-3xl font-bold bg-gradient-to-r from-purple-600 to-purple-800 bg-clip-text text-transparent">
SPJ Komite
</h1>
<p class="text-gray-500 mt-2">SMA Negeri 1 Abiansemal</p>
</div>
<!-- Login Form -->
<form id="loginForm" class="space-y-5">
<div class="fade-in fade-in-delay-2">
<label for="email" class="block text-sm font-semibold text-gray-700 mb-2">
<div class="flex items-center gap-2">
<i data-lucide="user" class="w-4 h-4"></i>
User
</div>
</label>
<div class="relative">
<input type="text" id="email" name="email" required placeholder="Masukkan username"
class="input-field w-full px-4 py-3.5 border border-gray-200 rounded-xl outline-none text-gray-700">
</div>
</div>
<div class="fade-in fade-in-delay-3">
<label for="password" class="block text-sm font-semibold text-gray-700 mb-2">
<div class="flex items-center gap-2">
<i data-lucide="lock" class="w-4 h-4"></i>
Password
</div>
</label>
<div class="relative">
<input type="password" id="password" name="password" required placeholder="••••••••"
class="input-field w-full px-4 py-3.5 border border-gray-200 rounded-xl outline-none text-gray-700">
<button type="button" id="togglePassword"
class="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600">
<i data-lucide="eye" class="w-5 h-5" id="eyeIcon"></i>
</button>
</div>
</div>
<div id="errorMessage"
class="hidden p-4 bg-red-50 border border-red-200 text-red-600 rounded-xl text-sm flex items-center gap-3">
<i data-lucide="alert-circle" class="w-5 h-5 flex-shrink-0"></i>
<span>User atau password salah. Silakan coba lagi.</span>
</div>
<button type="submit"
class="login-btn w-full py-4 text-white font-semibold rounded-xl flex items-center justify-center gap-2 text-lg fade-in fade-in-delay-4">
<i data-lucide="log-in" class="w-5 h-5"></i>
<span>Masuk</span>
</button>
</form>
<!-- Footer -->
<p class="text-center text-gray-400 text-xs mt-8 fade-in fade-in-delay-4">
© 2026 SPJ Komite - SMA Negeri 1 Abiansemal
</p>
</div>
</div>
</div>
<script>
// Initialize Lucide icons
lucide.createIcons();
// Demo credentials
const DEMO_USERS = [
{ email: 'adminkomite', password: '$manab#1', role: 'admin', name: 'Admin Komite' },
];
// Check if already authenticated
if (sessionStorage.getItem('isAuthenticated') === 'true') {
window.location.href = '/';
}
// Toggle password visibility
document.getElementById('togglePassword').addEventListener('click', function () {
const passwordInput = document.getElementById('password');
const eyeIcon = document.getElementById('eyeIcon');
if (passwordInput.type === 'password') {
passwordInput.type = 'text';
eyeIcon.setAttribute('data-lucide', 'eye-off');
} else {
passwordInput.type = 'password';
eyeIcon.setAttribute('data-lucide', 'eye');
}
lucide.createIcons();
});
// Handle login form submission
document.getElementById('loginForm').addEventListener('submit', async function (e) {
e.preventDefault();
const email = document.getElementById('email').value.trim();
const password = document.getElementById('password').value;
const errorMessage = document.getElementById('errorMessage');
const submitBtn = this.querySelector('button[type="submit"]');
errorMessage.classList.add('hidden');
submitBtn.disabled = true;
submitBtn.innerHTML = '<svg class="animate-spin w-5 h-5" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg><span>Memproses...</span>';
await new Promise(resolve => setTimeout(resolve, 1000));
const user = DEMO_USERS.find(u => u.email === email && u.password === password);
if (user) {
sessionStorage.setItem('isAuthenticated', 'true');
sessionStorage.setItem('userRole', user.role);
sessionStorage.setItem('userName', user.name);
window.location.href = '/';
} else {
errorMessage.classList.remove('hidden');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i data-lucide="log-in" class="w-5 h-5"></i><span>Masuk</span>';
lucide.createIcons();
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,3 @@
Kode,Uraian
400.3.8.1,Kurikulum
400.3.8.2,Bahan Ajar
1 Kode Uraian
2 400.3.8.1 Kurikulum
3 400.3.8.2 Bahan Ajar

View File

@@ -0,0 +1,4 @@
Pekerjaan
"Pengadaan Alat Tulis Kantor (ATK)"
"Pengadaan Bahan Praktik Siswa"
"Pengadaan Alat Kebersihan dan Bahan Pembersih"
1 Pekerjaan
2 Pengadaan Alat Tulis Kantor (ATK)
3 Pengadaan Bahan Praktik Siswa
4 Pengadaan Alat Kebersihan dan Bahan Pembersih

1270
spj-komite/server.js Normal file

File diff suppressed because it is too large Load Diff