- {r.code}
+ {" "}
+ {r.code}
{/* Per-item overclaim label */}
{(() => {
const rule = getIntervalRuleForCode(r.code);
- const isOverclaim = rule && daysSinceLast !== null && (daysSinceLast as number) < (rule as { minDays: number }).minDays;
+ const isOverclaim =
+ rule &&
+ daysSinceLast !== null &&
+ (daysSinceLast as number) <
+ (rule as { minDays: number }).minDays;
if (!isOverclaim) return null;
return (
- Interval < {String((rule as { minDays: number }).minDays)} hari ({String((rule as { note: string }).note)})
+ Interval <{" "}
+ {String((rule as { minDays: number }).minDays)}{" "}
+ hari ({String((rule as { note: string }).note)})
);
})()}
- {r.description}
+
+ {r.description}
+
- {r.confidence}%
+
+ {r.confidence}%
+
- {r.reasoning}
+
+ {r.reasoning}
+
- Belum ada rekomendasi
- Isi diagnosa/prosedur lalu klik "Buat Rekomendasi".
+
+ Belum ada rekomendasi
+
+
+ Isi diagnosa/prosedur lalu klik "Buat Rekomendasi".
+
)}
@@ -1101,35 +1238,55 @@ export default function CostRecommendation() {
{/* BPJS mapping and cost section */}
-
Rekomendasi Kode BPJS & Estimasi Biaya
+
+ Rekomendasi Kode BPJS & Estimasi Biaya
+
Mapping dari ICD
{isLoading ? (
-
Menyiapkan mapping BPJS...
+
+ Menyiapkan mapping BPJS...
+
) : results.length > 0 ? (
bpjsRecommendations.length > 0 ? (
- Kode ICD
- Kode BPJS
- Deskripsi
- Estimasi Tarif
+
+ Kode ICD
+
+
+ Kode BPJS
+
+
+ Deskripsi
+
+
+ Estimasi Tarif
+
{bpjsRecommendations.map(({ icdCode, mapping }) => (
-
+
- {icdCode}
+ {" "}
+ {icdCode}
- {mapping.bpjsCode}
+
+ {mapping.bpjsCode}
+
+
+
+ {mapping.description}
- {mapping.description}
@@ -1144,15 +1301,24 @@ export default function CostRecommendation() {
) : (
-
Belum ada mapping BPJS untuk rekomendasi ICD saat ini
-
Coba perbarui diagnosa/prosedur untuk melihat rekomendasi BPJS.
+
+ Belum ada mapping BPJS untuk rekomendasi ICD saat ini
+
+
+ Coba perbarui diagnosa/prosedur untuk melihat rekomendasi
+ BPJS.
+
)
) : (
-
Belum ada rekomendasi
-
Buat rekomendasi ICD terlebih dahulu.
+
+ Belum ada rekomendasi
+
+
+ Buat rekomendasi ICD terlebih dahulu.
+
)}
diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx
index b2b99b8..474bc69 100644
--- a/src/pages/Dashboard.tsx
+++ b/src/pages/Dashboard.tsx
@@ -246,93 +246,6 @@ export default function Dashboard() {
-
- {/* Additional Analytics Row */}
-
-
-
- Pasien Datang Hari Ini
-
-
-
- Rawat Jalan
- 52
-
-
- Rawat Inap
- 23
-
-
- IGD
- 14
-
-
-
-
-
-
-
- Potential Overclaim
-
-
-
- High Risk
- 3
-
-
- Medium Risk
- 4
-
-
- Low Risk
- 8
-
-
-
-
- Perlu Review
-
- 7
-
-
-
-
-
-
-
- Verifikasi Klaim
-
-
-
- Disetujui
- 89
-
-
- Pending
- 23
-
-
- Ditolak
- 5
-
-
-
-
- Total Klaim
-
- 117
-
-
-
-
-
);
diff --git a/src/pages/MedicalRecord.tsx b/src/pages/MedicalRecord.tsx
index f2487f4..ec0169d 100644
--- a/src/pages/MedicalRecord.tsx
+++ b/src/pages/MedicalRecord.tsx
@@ -2,18 +2,12 @@ import { useState } from "react";
import {
FileText,
Search,
- Filter,
- Plus,
Eye,
- Edit,
Calendar,
User,
Stethoscope,
Heart,
Activity,
- ClipboardList,
- Download,
- Printer,
} from "lucide-react";
interface MedicalRecord {
@@ -37,6 +31,14 @@ interface MedicalRecord {
};
}
+const dateDaysAgo = (days: number, hour = 10, minute = 30) => {
+ const d = new Date();
+ d.setHours(0, 0, 0, 0);
+ d.setDate(d.getDate() - days);
+ d.setHours(hour, minute, 0, 0);
+ return d.toISOString();
+};
+
const sampleMedicalRecords: MedicalRecord[] = [
{
id: "MR001",
@@ -44,7 +46,7 @@ const sampleMedicalRecords: MedicalRecord[] = [
patientName: "Ahmad Rizki",
patientAge: 45,
patientGender: "Laki-laki",
- recordDate: "2024-01-15T10:30:00Z",
+ recordDate: dateDaysAgo(5, 10, 30),
diagnosis: "Hipertensi Grade 2",
icdCode: "I10",
treatment: "Amlodipine 10mg 1x1, Diet rendah garam",
@@ -64,7 +66,7 @@ const sampleMedicalRecords: MedicalRecord[] = [
patientName: "Maria Lopez",
patientAge: 32,
patientGender: "Perempuan",
- recordDate: "2024-01-15T14:15:00Z",
+ recordDate: dateDaysAgo(12, 14, 15),
diagnosis: "Gastritis Akut",
icdCode: "K29.0",
treatment: "Omeprazole 20mg 2x1, Antasida 3x1",
@@ -84,7 +86,7 @@ const sampleMedicalRecords: MedicalRecord[] = [
patientName: "Dewi Sartika",
patientAge: 28,
patientGender: "Perempuan",
- recordDate: "2024-01-15T09:45:00Z",
+ recordDate: dateDaysAgo(20, 9, 45),
diagnosis: "Kehamilan Normal G1P0A0",
icdCode: "Z34.0",
treatment: "Asam folat 1x1, Vitamin prenatal",
@@ -104,7 +106,7 @@ const sampleMedicalRecords: MedicalRecord[] = [
patientName: "Joko Widodo",
patientAge: 56,
patientGender: "Laki-laki",
- recordDate: "2024-01-15T16:20:00Z",
+ recordDate: dateDaysAgo(28, 16, 20),
diagnosis: "Diabetes Mellitus Tipe 2",
icdCode: "E11.9",
treatment: "Metformin 500mg 2x1, Diet DM",
@@ -118,37 +120,64 @@ const sampleMedicalRecords: MedicalRecord[] = [
weight: 82,
},
},
+ {
+ id: "MR005",
+ patientId: "P004",
+ patientName: "Joko Widodo",
+ patientAge: 56,
+ patientGender: "Laki-laki",
+ recordDate: dateDaysAgo(7, 11, 10),
+ diagnosis: "Kontrol DM tipe 2, evaluasi terapi",
+ icdCode: "E11.9",
+ treatment: "Metformin 850mg 2x1, Edukasi diet & aktivitas",
+ doctor: "Dr. Budi Santoso",
+ department: "Endocrinology",
+ status: "reviewed",
+ vital: {
+ bloodPressure: "130/85",
+ heartRate: 78,
+ temperature: 36.6,
+ weight: 81,
+ },
+ },
];
export default function MedicalRecord() {
const [records] = useState(sampleMedicalRecords);
- const [searchTerm, setSearchTerm] = useState("");
- const [selectedDepartment, setSelectedDepartment] = useState("");
- const [selectedStatus, setSelectedStatus] = useState("");
+ const [nameInput, setNameInput] = useState("");
+ const [idInput, setIdInput] = useState("");
+ const [appliedName, setAppliedName] = useState("");
+ const [appliedId, setAppliedId] = useState("");
+ const [hasSearched, setHasSearched] = useState(false);
+ const [detailId, setDetailId] = useState(null);
- const departments = [
- "Cardiology",
- "Internal Medicine",
- "Obstetrics & Gynecology",
- "Endocrinology",
- "Emergency",
- "Surgery",
- "Pediatrics",
- ];
+ // departments removed (unused)
+
+ const handleSearch = () => {
+ setAppliedName(nameInput.trim());
+ setAppliedId(idInput.trim());
+ setHasSearched(true);
+ };
+
+ const handleReset = () => {
+ setNameInput("");
+ setIdInput("");
+ setAppliedName("");
+ setAppliedId("");
+ setHasSearched(false);
+ };
const filteredRecords = records.filter((record) => {
- const matchesSearch =
- record.patientName.toLowerCase().includes(searchTerm.toLowerCase()) ||
- record.diagnosis.toLowerCase().includes(searchTerm.toLowerCase()) ||
- record.icdCode.toLowerCase().includes(searchTerm.toLowerCase()) ||
- record.doctor.toLowerCase().includes(searchTerm.toLowerCase());
-
- const matchesDepartment =
- !selectedDepartment || record.department === selectedDepartment;
-
- const matchesStatus = !selectedStatus || record.status === selectedStatus;
-
- return matchesSearch && matchesDepartment && matchesStatus;
+ const thirtyDaysAgo = new Date();
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
+ const inLast30Days = new Date(record.recordDate) >= thirtyDaysAgo;
+ if (!hasSearched) return inLast30Days;
+ const matchName =
+ !appliedName ||
+ record.patientName.toLowerCase().includes(appliedName.toLowerCase());
+ const matchId =
+ !appliedId || record.patientId.toLowerCase() === appliedId.toLowerCase();
+ return inLast30Days && matchName && matchId;
});
const getStatusColor = (status: string) => {
@@ -207,109 +236,62 @@ export default function MedicalRecord() {
-
-
-
- Export Data
-
-
-
- Tambah Record
-
-
+
- {/* Stats Cards */}
-
-
-
-
-
- Total Records
-
-
- {records.length}
-
-
-
-
-
-
-
-
-
Completed
-
- {records.filter((r) => r.status === "completed").length}
-
-
-
-
-
-
-
-
-
Reviewed
-
- {records.filter((r) => r.status === "reviewed").length}
-
-
-
-
-
-
-
-
-
Draft
-
- {records.filter((r) => r.status === "draft").length}
-
-
-
-
-
-
-
- {/* Filters and Search */}
+ {/* Filters: Search by Patient Name and Patient ID with trigger button */}
-
-
+
+
+
+ Cari Nama Pasien
+
setSearchTerm(e.target.value)}
- className="pl-9 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-transparent w-80"
+ placeholder="Contoh: Ahmad Rizki"
+ value={nameInput}
+ onChange={(e) => setNameInput(e.target.value)}
+ className="pl-9 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-transparent w-full"
/>
-
-
- setSelectedDepartment(e.target.value)}
- className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-green-500 focus:border-transparent"
- >
- Semua Department
- {departments.map((dept) => (
-
- {dept}
-
- ))}
-
- setSelectedStatus(e.target.value)}
- className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-green-500 focus:border-transparent"
- >
- Semua Status
- Draft
- Selesai
- Direview
-
+
+
+
+ Cari ID Pasien
+
+
+
+ setIdInput(e.target.value)}
+ className="pl-9 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-transparent w-full"
+ />
+
+
+
+ Cari
+
+
+ Reset
+
+
+
+
+ Menampilkan riwayat medical record 30 hari terakhir sesuai kriteria.
@@ -331,14 +313,11 @@ export default function MedicalRecord() {
Dokter & Dept
-
- Status
-
Tanggal
- Aksi
+ Detail
@@ -347,13 +326,6 @@ export default function MedicalRecord() {
-
- {record.patientName
- .split(" ")
- .map((n) => n[0])
- .join("")
- .toUpperCase()}
-
{record.patientName}
@@ -408,21 +380,9 @@ export default function MedicalRecord() {
{record.doctor}
-
- {record.department}
-
-
-
- {getStatusText(record.status)}
-
-
@@ -431,18 +391,13 @@ export default function MedicalRecord() {
-
+ setDetailId(record.id)}
+ className="text-blue-600 hover:text-blue-900"
+ title="Lihat detail rekam medis"
+ >
-
-
-
-
-
-
-
-
-
@@ -452,8 +407,302 @@ export default function MedicalRecord() {
+ {/* Detail Modal */}
+ {detailId && (
+
+
+
+
+ Detail Rekam Medis
+
+
setDetailId(null)}
+ className="text-gray-500 hover:text-gray-700 text-sm"
+ >
+ Tutup
+
+
+ {(() => {
+ const rec = records.find((r) => r.id === detailId);
+ if (!rec)
+ return (
+
Data tidak ditemukan.
+ );
+
+ // Dummy detail menyerupai rekam medis standar RS (hasil olahan LLM)
+ const details = (() => {
+ switch (rec.icdCode) {
+ case "I10":
+ return {
+ chiefComplaint: "Pusing dan tekanan darah tinggi",
+ hpi: "Keluhan sejak 3 hari, pusing berdenyut terutama pagi hari. Tidak ada nyeri dada/sesak.",
+ pmh: ["Hipertensi 2 tahun", "Dislipidemia"],
+ meds: ["Amlodipine 10mg 1x1", "Atorvastatin 20mg 1x1"],
+ allergies: ["Tidak ada"],
+ labs: [
+ "Profil lipid: LDL 140 mg/dl",
+ "Fungsi ginjal normal",
+ ],
+ imaging: ["Foto toraks dalam batas normal"],
+ procedures: [
+ "Edukasi diet rendah garam",
+ "Monitoring TD rumah",
+ ],
+ plan: [
+ "Lanjut obat, evaluasi 2 minggu",
+ "Olahraga 30 menit/hari",
+ "Kontrol tekanan darah harian",
+ ],
+ discharge:
+ "Pulang kondisi stabil, edukasi dan rencana kontrol diberikan.",
+ } as const;
+ case "K29.0":
+ return {
+ chiefComplaint: "Nyeri ulu hati dan mual",
+ hpi: "Nyeri ulu hati terutama setelah makan pedas/asam, mual tanpa muntah.",
+ pmh: ["Gastritis episodik"],
+ meds: ["Omeprazole 20mg 2x1", "Antasida 3x1"],
+ allergies: ["Tidak ada"],
+ labs: ["Hb normal", "Helicobacter pylori: negatif"],
+ imaging: ["Tidak dilakukan"],
+ procedures: ["Edukasi diet lambung", "Hindari NSAID"],
+ plan: [
+ "PPI 2 minggu",
+ "Diet lunak, porsi kecil",
+ "Kontrol bila nyeri menetap",
+ ],
+ discharge: "Pulang, kontrol bila keluhan berlanjut.",
+ } as const;
+ case "E11.9":
+ return {
+ chiefComplaint: "Sering haus dan berkemih",
+ hpi: "Keluhan polidipsia dan poliuria 1 minggu, gula darah rumah meningkat.",
+ pmh: ["DM tipe 2 3 tahun"],
+ meds: ["Metformin 500mg 2x1"],
+ allergies: ["Tidak ada"],
+ labs: ["GDP 180 mg/dl", "HbA1c 8.1%"],
+ imaging: ["Tidak dilakukan"],
+ procedures: ["Edukasi diet DM", "Jurnal gula harian"],
+ plan: [
+ "Optimasi metformin",
+ "Rencana edukasi diet dan aktivitas",
+ "Kontrol 2-4 minggu",
+ ],
+ discharge:
+ "Pulang, monitoring gula dan kontrol terjadwal.",
+ } as const;
+ default:
+ return {
+ chiefComplaint: "Keluhan sesuai diagnosa utama",
+ hpi: "Riwayat penyakit sekarang sesuai catatan klinis.",
+ pmh: ["Tidak ada yang menonjol"],
+ meds: [rec.treatment],
+ allergies: ["Tidak diketahui"],
+ labs: ["Dalam batas normal"],
+ imaging: ["Tidak ada temuan bermakna"],
+ procedures: ["Observasi dan edukasi"],
+ plan: ["Kontrol sesuai kebutuhan"],
+ discharge: "Pulang dalam kondisi stabil.",
+ } as const;
+ }
+ })();
+
+ return (
+
+
+
+
+ Identitas Pasien & Kunjungan
+
+
+
+
+ Nama / Jenis Kelamin / Umur
+
+
+ {rec.patientName} ({rec.patientGender},{" "}
+ {rec.patientAge} th)
+
+
+ ID Pasien: {rec.patientId}
+
+
+
+
+ Tanggal / Dokter / Dept
+
+
+ {formatDate(rec.recordDate)}
+
+
+ {rec.doctor} • {rec.department}
+
+
+
+
+
+
+
+ Anamnesis
+
+
+
+
+ Keluhan Utama
+
+
+ {details.chiefComplaint}
+
+
+
+
+ Riwayat Penyakit Sekarang
+
+
+ {details.hpi}
+
+
+
+
+
+
+
+ Riwayat / Obat / Alergi
+
+
+
+
+ Riwayat Penyakit Dahulu
+
+
+ {details.pmh.map((x, i) => (
+ {x}
+ ))}
+
+
+
+
+ Obat
+
+
+ {details.meds.map((x, i) => (
+ {x}
+ ))}
+
+
+
+
+ Alergi
+
+
+ {details.allergies.map((x, i) => (
+ {x}
+ ))}
+
+
+
+
+
+
+
+ Penunjang
+
+
+
+
+ Laboratorium
+
+
+ {details.labs.map((x, i) => (
+ {x}
+ ))}
+
+
+
+
+ Imaging
+
+
+ {details.imaging.map((x, i) => (
+ {x}
+ ))}
+
+
+
+
+
+
+
+ Diagnosis & Tindakan
+
+
+
+
+ Diagnosa & ICD
+
+
{rec.diagnosis}
+
+ {rec.icdCode}
+
+
+
+
+ Prosedur/Tindakan
+
+
+ {details.procedures.map((x, i) => (
+ {x}
+ ))}
+
+
+
+
+
+
+
+ Tanda Vital & Rencana
+
+
+
+
+ TD: {rec.vital.bloodPressure} mmHg
+ Nadi: {rec.vital.heartRate} bpm
+ Suhu: {rec.vital.temperature}°C
+ Berat: {rec.vital.weight} kg
+
+
+
+
+ {details.plan.map((x, i) => (
+ {x}
+ ))}
+
+
+
+
+
+
+
+ Status: {getStatusText(rec.status)}
+
+
+ Ringkasan Pulang: {details.discharge}
+
+
+
+
+ );
+ })()}
+
+
+ )}
+
{/* Empty State */}
- {filteredRecords.length === 0 && (
+ {hasSearched && filteredRecords.length === 0 && (
diff --git a/src/pages/MedicalRecordSync.tsx b/src/pages/MedicalRecordSync.tsx
index 00b4f3d..eadeb4b 100644
--- a/src/pages/MedicalRecordSync.tsx
+++ b/src/pages/MedicalRecordSync.tsx
@@ -3,7 +3,6 @@ import {
Database,
RefreshCw,
Calendar,
- Search,
Filter,
CheckCircle,
XCircle,
@@ -45,10 +44,15 @@ interface SyncStats {
}
export default function MedicalRecordSync() {
- const [searchTerm, setSearchTerm] = useState("");
- const [statusFilter, setStatusFilter] = useState("all");
+ const [statusInput, setStatusInput] = useState("all");
+ const [appliedStatus, setAppliedStatus] = useState("all");
const [isImporting, setIsImporting] = useState(false);
const [isSyncing, setIsSyncing] = useState(false);
+ const [startDateInput, setStartDateInput] = useState("");
+ const [endDateInput, setEndDateInput] = useState("");
+ const [appliedStartDate, setAppliedStartDate] = useState(null);
+ const [appliedEndDate, setAppliedEndDate] = useState(null);
+ const [hasDateFiltered, setHasDateFiltered] = useState(false);
const formatDate = (dateString: string) => {
const date = new Date(dateString);
@@ -207,17 +211,23 @@ export default function MedicalRecordSync() {
syncLogs.filter((log) => log.duration > 0).length || 0,
};
- // Filter logs based on search and status
+ // Filter logs based on date and status (status applied via button)
const filteredLogs = syncLogs.filter((log) => {
- const matchesSearch =
- log.source.toLowerCase().includes(searchTerm.toLowerCase()) ||
- log.type.toLowerCase().includes(searchTerm.toLowerCase()) ||
- (log.errorMessage &&
- log.errorMessage.toLowerCase().includes(searchTerm.toLowerCase()));
+ const matchesStatus =
+ appliedStatus === "all" || log.status === appliedStatus;
+ let matchesDate = true;
+ if (hasDateFiltered) {
+ const ts = new Date(log.timestamp).getTime();
+ const startOk =
+ !appliedStartDate ||
+ ts >= new Date(appliedStartDate + "T00:00:00").getTime();
+ const endOk =
+ !appliedEndDate ||
+ ts <= new Date(appliedEndDate + "T23:59:59").getTime();
+ matchesDate = startOk && endOk;
+ }
- const matchesStatus = statusFilter === "all" || log.status === statusFilter;
-
- return matchesSearch && matchesStatus;
+ return matchesStatus && matchesDate;
});
const handleImport = async () => {
@@ -346,25 +356,33 @@ export default function MedicalRecordSync() {
- {/* Filters and Search */}
+ {/* Filters */}
-
-
-
+
+ {/* Date range */}
+
+
setSearchTerm(e.target.value)}
- className="pl-9 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent w-80"
+ type="date"
+ value={startDateInput}
+ onChange={(e) => setStartDateInput(e.target.value)}
+ className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+ />
+ s/d
+ setEndDateInput(e.target.value)}
+ className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
+ {/* Status */}
setStatusFilter(e.target.value)}
+ value={statusInput}
+ onChange={(e) => setStatusInput(e.target.value)}
className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
Semua Status
@@ -373,6 +391,34 @@ export default function MedicalRecordSync() {
Berlangsung
+ {/* Buttons */}
+
+ {
+ setAppliedStartDate(startDateInput || null);
+ setAppliedEndDate(endDateInput || null);
+ setHasDateFiltered(Boolean(startDateInput || endDateInput));
+ setAppliedStatus(statusInput);
+ }}
+ className="btn-primary px-3 py-2"
+ >
+ Filter
+
+ {
+ setStartDateInput("");
+ setEndDateInput("");
+ setAppliedStartDate(null);
+ setAppliedEndDate(null);
+ setHasDateFiltered(false);
+ setStatusInput("all");
+ setAppliedStatus("all");
+ }}
+ className="btn-secondary px-3 py-2 border border-gray-300 rounded-md"
+ >
+ Reset
+
+
@@ -406,9 +452,6 @@ export default function MedicalRecordSync() {
Duration
-
- Details
-
@@ -507,26 +550,6 @@ export default function MedicalRecordSync() {
{log.duration > 0 ? `${log.duration}s` : "-"}
-
-
-
-
- Pasien: {log.details.patientsUpdated}
-
-
-
- Diagnosis: {log.details.diagnosesAdded}
-
-
-
- Treatment: {log.details.treatmentsAdded}
-
-
-
- Vitals: {log.details.vitalsUpdated}
-
-
-
))}
diff --git a/src/pages/Patients.tsx b/src/pages/Patients.tsx
deleted file mode 100644
index 9508867..0000000
--- a/src/pages/Patients.tsx
+++ /dev/null
@@ -1,284 +0,0 @@
-import {
- Users,
- UserPlus,
- Search,
- Filter,
- Calendar,
- Eye,
- Edit,
- Download,
- Printer,
- User,
- Phone,
-} from "lucide-react";
-
-export default function Patients() {
- const patients = [
- {
- id: "P001",
- name: "Ahmad Rizki",
- age: 35,
- gender: "Laki-laki",
- phone: "+62 812-3456-7890",
- lastVisit: "2024-01-15T10:30:00Z",
- status: "Active",
- },
- {
- id: "P002",
- name: "Siti Nurhaliza",
- age: 28,
- gender: "Perempuan",
- phone: "+62 813-4567-8901",
- lastVisit: "2024-01-14T14:15:00Z",
- status: "Active",
- },
- {
- id: "P003",
- name: "Budi Santoso",
- age: 42,
- gender: "Laki-laki",
- phone: "+62 814-5678-9012",
- lastVisit: "2024-01-10T09:45:00Z",
- status: "Inactive",
- },
- {
- id: "P004",
- name: "Maria Lopez",
- age: 29,
- gender: "Perempuan",
- phone: "+62 815-6789-0123",
- lastVisit: "2024-01-13T16:20:00Z",
- status: "Active",
- },
- {
- id: "P005",
- name: "Dewi Sartika",
- age: 31,
- gender: "Perempuan",
- phone: "+62 816-7890-1234",
- lastVisit: "2024-01-12T11:30:00Z",
- status: "Active",
- },
- ];
-
- const formatDate = (dateString: string) => {
- const date = new Date(dateString);
- return date.toLocaleDateString("id-ID", {
- day: "numeric",
- month: "short",
- year: "numeric",
- hour: "2-digit",
- minute: "2-digit",
- });
- };
-
- return (
-
-
- {/* Header */}
-
-
-
-
-
-
-
-
- Manajemen Pasien
-
-
- Kelola data pasien rumah sakit dan informasi kontak
-
-
-
-
-
-
- Export Data
-
-
-
- Tambah Pasien
-
-
-
-
-
- {/* Stats Cards */}
-
-
- {/* Filters and Search */}
-
-
-
-
-
-
-
-
-
-
- Semua Status
- Aktif
- Tidak Aktif
-
-
- Semua Gender
- Laki-laki
- Perempuan
-
-
-
-
-
-
- {/* Patients Table */}
-
-
-
-
-
-
- Pasien
-
-
- Kontak
-
-
- Status
-
-
- Kunjungan Terakhir
-
-
- Aksi
-
-
-
-
- {patients.map((patient) => (
-
-
-
-
- {patient.name
- .split(" ")
- .map((n) => n[0])
- .join("")
- .toUpperCase()}
-
-
-
- {patient.name}
-
-
-
- {patient.age} tahun • {patient.gender}
-
-
- ID: {patient.id}
-
-
-
-
-
-
-
-
-
- {patient.status === "Active" ? "Aktif" : "Tidak Aktif"}
-
-
-
-
-
- {formatDate(patient.lastVisit)}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))}
-
-
-
-
-
-
- );
-}
diff --git a/src/pages/RoleManagement.tsx b/src/pages/RoleManagement.tsx
index 977626d..4a9eb2a 100644
--- a/src/pages/RoleManagement.tsx
+++ b/src/pages/RoleManagement.tsx
@@ -7,11 +7,8 @@ import {
Search,
Filter,
Users,
- CheckCircle,
XCircle,
- Settings,
Calendar,
- Download,
} from "lucide-react";
import { sampleRoles, MODULES, ACTIONS } from "../types/roles";
import type { IRole } from "../types/roles";
@@ -34,22 +31,28 @@ const getModuleDisplayName = (module: string) => {
export default function RoleManagement() {
const [roles, setRoles] = useState
(sampleRoles);
- const [searchTerm, setSearchTerm] = useState("");
+ const [searchInput, setSearchInput] = useState("");
const [selectedRole, setSelectedRole] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalMode, setModalMode] = useState<"create" | "edit">("create");
- const [statusFilter, setStatusFilter] = useState("all");
- const [permissionFilter, setPermissionFilter] = useState("all");
+ const [statusInput, setStatusInput] = useState("all");
+ const [permissionFilter] = useState("all");
+ const [appliedSearch, setAppliedSearch] = useState("");
+ const [appliedStatus, setAppliedStatus] = useState("all");
+ const [hasFiltered, setHasFiltered] = useState(false);
const filteredRoles = roles.filter((role) => {
- const matchesSearch =
- role.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
- role.description.toLowerCase().includes(searchTerm.toLowerCase());
+ const matchesSearch = !hasFiltered
+ ? true
+ : !appliedSearch ||
+ role.name.toLowerCase().includes(appliedSearch.toLowerCase()) ||
+ role.description.toLowerCase().includes(appliedSearch.toLowerCase());
+ const statusToUse = hasFiltered ? appliedStatus : "all";
const matchesStatus =
- statusFilter === "all" ||
- (statusFilter === "active" && role.isActive) ||
- (statusFilter === "inactive" && !role.isActive);
+ statusToUse === "all" ||
+ (statusToUse === "active" && role.isActive) ||
+ (statusToUse === "inactive" && !role.isActive);
const matchesPermission =
permissionFilter === "all" ||
@@ -119,10 +122,6 @@ export default function RoleManagement() {
-
-
- Export Data
-
- {/* Stats Cards */}
-
-
-
-
-
Total Roles
-
- {roles.length}
-
-
-
-
-
-
-
-
-
- Active Roles
-
-
- {roles.filter((r) => r.isActive).length}
-
-
-
-
-
-
-
-
-
- Inactive Roles
-
-
- {roles.filter((r) => !r.isActive).length}
-
-
-
-
-
-
-
-
-
- Avg Permissions
-
-
- {Math.round(
- roles.reduce(
- (acc, role) => acc + role.permissions.length,
- 0
- ) / roles.length
- )}
-
-
-
-
-
-
-
- {/* Filters and Search */}
+ {/* Filters */}
-
+
+ {/* Search */}
setSearchTerm(e.target.value)}
+ value={searchInput}
+ onChange={(e) => setSearchInput(e.target.value)}
className="pl-9 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500 focus:border-transparent w-80"
/>
+ {/* Status */}
setStatusFilter(e.target.value)}
+ value={statusInput}
+ onChange={(e) => setStatusInput(e.target.value)}
className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
Semua Status
Aktif
Tidak Aktif
- setPermissionFilter(e.target.value)}
- className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
+
+ {/* Buttons */}
+
+ {
+ setAppliedSearch(searchInput.trim());
+ setAppliedStatus(statusInput);
+ setHasFiltered(true);
+ }}
+ className="btn-primary px-3 py-2"
>
- Semua Permission
- Banyak (8+)
- Sedang (4-7)
- Sedikit (1-3)
-
+ Filter
+
+ {
+ setSearchInput("");
+ setStatusInput("all");
+ setAppliedSearch("");
+ setAppliedStatus("all");
+ setHasFiltered(false);
+ }}
+ className="btn-secondary px-3 py-2 border border-gray-300 rounded-md"
+ >
+ Reset
+
diff --git a/src/pages/UserManagement.tsx b/src/pages/UserManagement.tsx
index 7969ee3..e03320b 100644
--- a/src/pages/UserManagement.tsx
+++ b/src/pages/UserManagement.tsx
@@ -8,55 +8,39 @@ import {
Filter,
Mail,
Phone,
- Building2,
XCircle,
- UserCheck,
- Clock,
Shield,
Calendar,
- Download,
} from "lucide-react";
import { sampleUsers, sampleRoles } from "../types/roles";
import type { IUser } from "../types/roles";
export default function UserManagement() {
const [users, setUsers] = useState
(sampleUsers);
- const [searchTerm, setSearchTerm] = useState("");
+ const [searchInput, setSearchInput] = useState("");
const [selectedUser, setSelectedUser] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalMode, setModalMode] = useState<"create" | "edit">("create");
- const [selectedDepartment, setSelectedDepartment] = useState("");
- const [selectedStatus, setSelectedStatus] = useState("");
-
- const departments = [
- "IT & Administration",
- "Administration",
- "Cardiology",
- "Emergency",
- "Finance",
- "Pharmacy",
- "Laboratory",
- "Radiology",
- "Surgery",
- "Pediatrics",
- ];
+ const [statusInput, setStatusInput] = useState("");
+ const [appliedSearch, setAppliedSearch] = useState("");
+ const [appliedStatus, setAppliedStatus] = useState("");
+ const [hasFiltered, setHasFiltered] = useState(false);
const filteredUsers = users.filter((user) => {
- const matchesSearch =
- user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
- user.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
- user.department.toLowerCase().includes(searchTerm.toLowerCase()) ||
- user.role.name.toLowerCase().includes(searchTerm.toLowerCase());
-
- const matchesDepartment =
- !selectedDepartment || user.department === selectedDepartment;
+ const matchesSearch = !hasFiltered
+ ? true
+ : !appliedSearch ||
+ user.name.toLowerCase().includes(appliedSearch.toLowerCase()) ||
+ user.email.toLowerCase().includes(appliedSearch.toLowerCase()) ||
+ user.role.name.toLowerCase().includes(appliedSearch.toLowerCase());
+ const statusToUse = hasFiltered ? appliedStatus : "";
const matchesStatus =
- !selectedStatus ||
- (selectedStatus === "active" && user.isActive) ||
- (selectedStatus === "inactive" && !user.isActive);
+ !statusToUse ||
+ (statusToUse === "active" && user.isActive) ||
+ (statusToUse === "inactive" && !user.isActive);
- return matchesSearch && matchesDepartment && matchesStatus;
+ return matchesSearch && matchesStatus;
});
const handleCreateUser = () => {
@@ -121,10 +105,6 @@ export default function UserManagement() {
-
-
- Export Data
-
- {/* Stats Cards */}
-
-
-
-
-
Total Users
-
- {users.length}
-
-
-
-
-
-
-
-
-
- Active Users
-
-
- {users.filter((u) => u.isActive).length}
-
-
-
-
-
-
-
-
-
- Inactive Users
-
-
- {users.filter((u) => !u.isActive).length}
-
-
-
-
-
-
-
-
-
- Online Today
-
-
- {
- users.filter((u) => {
- if (!u.lastLogin) return false;
- const lastLogin = new Date(u.lastLogin);
- const today = new Date();
- return lastLogin.toDateString() === today.toDateString();
- }).length
- }
-
-
-
-
-
-
-
- {/* Filters and Search */}
+ {/* Filters */}
-
+
+ {/* Search */}
setSearchTerm(e.target.value)}
+ placeholder="Cari nama, email, atau role..."
+ value={searchInput}
+ onChange={(e) => setSearchInput(e.target.value)}
className="pl-9 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent w-80"
/>
+ {/* Status */}
setSelectedDepartment(e.target.value)}
- className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
- >
- Semua Department
- {departments.map((dept) => (
-
- {dept}
-
- ))}
-
- setSelectedStatus(e.target.value)}
+ value={statusInput}
+ onChange={(e) => setStatusInput(e.target.value)}
className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
Semua Status
@@ -235,6 +144,31 @@ export default function UserManagement() {
Tidak Aktif
+ {/* Buttons */}
+
+ {
+ setAppliedSearch(searchInput.trim());
+ setAppliedStatus(statusInput);
+ setHasFiltered(true);
+ }}
+ className="btn-primary px-3 py-2"
+ >
+ Filter
+
+ {
+ setSearchInput("");
+ setStatusInput("");
+ setAppliedSearch("");
+ setAppliedStatus("");
+ setHasFiltered(false);
+ }}
+ className="btn-secondary px-3 py-2 border border-gray-300 rounded-md"
+ >
+ Reset
+
+
@@ -251,9 +185,6 @@ export default function UserManagement() {
Role
-
- Department
-
Status
@@ -270,13 +201,6 @@ export default function UserManagement() {
-
- {user.name
- .split(" ")
- .map((n) => n[0])
- .join("")
- .toUpperCase()}
-
{user.name}
@@ -305,14 +229,6 @@ export default function UserManagement() {
-
-
-
-
- {user.department}
-
-
-
handleToggleStatus(user.id)}
@@ -428,23 +344,6 @@ export default function UserManagement() {
/>
-
-
- Department
-
-
- Pilih Department
- {departments.map((dept) => (
-
- {dept}
-
- ))}
-
-
-
Role