e.stopPropagation()}
- >
- {selectedFile.data && (
-
})
- )}
-
🪪 Detail Data Document
-
-
- }
- fileName={`Document_${selectedFile.nik || selectedFile.id || 'unknown'}.pdf`}
- style={{
- textDecoration: "none",
- padding: "8px 16px",
- color: "#fff",
- backgroundColor: "#00adef",
- borderRadius: "6px",
- display: "inline-block",
- }}
- >
- {({ loading }) =>
- loading ? "Menyiapkan PDF..." : "⬇️ Unduh PDF"
- }
-
+
e.stopPropagation()}>
+
+
+ Detail Data:{" "}
+ {selectedEntry.data?.[selectedType.entryName] ||
+ selectedEntry.data?.nama ||
+ selectedEntry.data?.name ||
+ "Data"}
+
+
-
-
- {selectedFile && (console.log("selectedFile in modal:", selectedFile), true) &&
- Object.entries(selectedFile)
- .map(([key, value]) => {
- console.log(`Processing: ${key} = ${value} (type: ${typeof value})`);
- return [key, value];
- })
- .filter(([key, value]) => {
- console.log(`Filtering: ${key} = ${value}`);
-
- // Exclude specific keys that are not part of the display data
- const excludedKeys = [
- "id",
- "document_type",
- "created_at",
- "data", // Exclude image data
- "foto_url", // Exclude image URL
- ];
-
- if (excludedKeys.includes(key)) {
- console.log(`Excluded key: ${key}`);
- return false;
- }
-
- if (value === null) {
- console.log(`Null value for key: ${key}`);
- return false;
- }
- if (value === undefined) {
- console.log(`Undefined value for key: ${key}`);
- return false;
- }
- if (typeof value === 'string' && value.trim() === '') {
- console.log(`Empty string for key: ${key}`);
- return false;
- }
-
- console.log(`Keeping key: ${key} with value: ${value}`);
- return true;
- })
- .map(([key, value]) => {
- console.log(`Rendering field: ${key} = ${value}`);
-
- // Special handling for 'anggota' array
- if (key === "anggota" && Array.isArray(value)) {
- return (
-
- | {formatKeyToLabel(key)} |
-
- {value.map((member, idx) => (
-
- {Object.entries(member)
- .filter(([_, memberValue]) => {
- if (memberValue === null || memberValue === undefined) return false;
- if (typeof memberValue === 'string' && memberValue.trim() === '') return false;
- return true;
- })
- .map(([memberKey, memberValue]) => (
-
- {formatKeyToLabel(memberKey)}: {memberValue}
-
- ))}
-
- ))}
- |
-
- );
- }
- // Format dates for display
- let displayValue = value;
- if (typeof value === 'string' &&
- (key.includes('tanggal') || key.includes('lahir') || key.includes('berlaku') || key.includes('pembuatan') || key.includes('created_at'))) {
- const date = new Date(value);
- if (!isNaN(date.getTime())) {
- displayValue = date.toLocaleDateString('id-ID');
- }
- }
-
- return (
-
- | {formatKeyToLabel(key)} |
- {displayValue} |
-
- );
- })}
-
-
-
+
+
+ {Object.entries(selectedEntry.data || {}).map(([key, value]) => (
+
+
+ {key
+ .replace(/_/g, " ")
+ .replace(/\b\w/g, (l) => l.toUpperCase())}
+
+
{value || "-"}
+
+ ))}
+
+
)}
diff --git a/src/FileListComponent.module.css b/src/FileListComponent.module.css
index bc0f14e..62dbd26 100644
--- a/src/FileListComponent.module.css
+++ b/src/FileListComponent.module.css
@@ -1,169 +1,95 @@
-/* FileListComponent.module.css - Updated to match Dashboard design */
+/* FileListComponent.module.css - Brand Blue/Indigo & Mobile-First */
-/* Use the same color palette as Dashboard */
+/* ===== CSS Variables ===== */
:root {
- --primary-blue: #3b82f6;
- --secondary-blue: #60a5fa;
- --dark-blue: #1e40af;
- --neutral-50: #fafafa;
- --neutral-100: #f5f5f5;
- --neutral-200: #e5e5e5;
- --neutral-300: #d4d4d4;
- --neutral-500: #737373;
- --neutral-700: #404040;
- --neutral-800: #262626;
- --neutral-900: #171717;
+ --brand-primary: #2961eb;
+ --brand-primary-700: #1d4ed8;
+ --brand-secondary: #4f46e5;
+ --brand-secondary-700: #4338ca;
+ --brand-gradient: linear-gradient(135deg, var(--brand-primary), var(--brand-secondary));
+
+ --neutral-25: #fcfcfd;
+ --neutral-50: #f9fafb;
+ --neutral-100: #f3f4f6;
+ --neutral-200: #e5e7eb;
+ --neutral-300: #d1d5db;
+ --neutral-400: #9ca3af;
+ --neutral-600: #475569;
+ --neutral-700: #374151;
+ --neutral-800: #1f2937;
--white: #ffffff;
- --success-green: #43a0a7;
- --warning-amber: #f59e0b;
- --error-red: #ef4444;
+
--text-primary: #0f172a;
--text-secondary: #64748b;
- --text-light: #ffffff;
- --border-light: #e2e8f0;
- --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
- --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
- --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1),
- 0 4px 6px -4px rgb(0 0 0 / 0.1);
+ --text-on-brand: #ffffff;
+
+ --border-light: #e5e7eb;
+ --shadow-sm: 0 1px 2px rgba(0,0,0,.06);
+ --shadow-md: 0 4px 10px rgba(2,6,23,.08);
+ --shadow-lg: 0 12px 22px rgba(2,6,23,.12);
+ --focus-ring: 0 0 0 3px rgba(37, 99, 235, .18);
}
-/* File List Section */
-.fileListSection {
- background-color: var(--white);
- padding: 2rem;
- border-radius: 1rem;
+/* ===== Container ===== */
+.container {
+ background: var(--white);
border: 1px solid var(--border-light);
+ border-radius: 1rem;
box-shadow: var(--shadow-sm);
- margin: 2rem auto;
- max-width: 1200px;
- width: 100%;
- overflow: hidden;
- max-height: 600px;
- height: auto;
- min-height: 0;
- display: flex;
- flex-direction: column;
- justify-content: flex-start;
- align-items: center;
- transition: all 0.2s ease;
-}
-
-.fileListSection:hover {
- box-shadow: var(--shadow-md);
-}
-
-.fileListHeader {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 1.5rem;
- flex-wrap: wrap;
- gap: 1rem;
- flex-shrink: 0;
- width: 100%;
-}
-
-.fileListTitle {
+ padding: 1.5rem;
margin: 0;
- font-size: 1.25rem;
- font-weight: 600;
- color: var(--text-primary);
- letter-spacing: -0.025em;
-}
-
-.fileCount {
- font-size: 0.6rem;
- color: #ffffff;
- font-weight: 500;
- background-color: #43a0a7;
- padding: 0.25rem 0.75rem;
- border-radius: 1rem;
- border: 1px solid var(--border-light);
-}
-
-.successMessage {
- background-color: rgb(67 160 167 / 0.1);
- color: var(--success-green);
- border: 1px solid rgb(67 160 167 / 0.2);
- padding: 0.75rem 1rem;
- border-radius: 0.5rem;
- margin-bottom: 1rem;
- font-size: 0.875rem;
- font-weight: 500;
- display: flex;
- align-items: center;
- gap: 0.5rem;
- flex-shrink: 0;
width: 100%;
+ min-width: 0;
+ overflow: hidden;
+ box-sizing: border-box;
}
-.tableContainer {
- flex: 1;
- overflow: auto;
- border-radius: 0.75rem;
- border: 1px solid var(--border-light);
- background-color: var(--white);
- width: 100%;
+/* ===== Title ===== */
+.title {
+ margin: 0 0 1.5rem 0;
+ font-size: clamp(1.125rem, 1vw + 0.8rem, 1.5rem);
+ font-weight: 800;
+ letter-spacing: -0.015em;
+ color: var(--neutral-800);
+ background: var(--brand-gradient);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
}
-.fileTable {
- width: 100%;
- min-width: 600px;
- table-layout: auto;
- border-collapse: collapse;
- font-size: 0.875rem;
- background-color: var(--white);
-}
-
-.fileTable th {
- background-color: #43a0a7;
- padding: 0.75rem;
+/* ===== Loading State ===== */
+.loading {
text-align: center;
- font-weight: 600;
- color: #ffffff;
- border-bottom: 1px solid var(--border-light);
- white-space: nowrap;
- position: sticky;
- top: 0;
- z-index: 1;
- font-size: 0.875rem;
- text-transform: uppercase;
- letter-spacing: 0.05em;
+ padding: 3rem 1rem;
+ color: var(--text-secondary);
}
-.fileTable td {
- padding: 0.75rem;
- border-bottom: 1px solid var(--border-light);
- color: var(--text-primary);
- vertical-align: middle;
+.spinner {
+ width: 2rem;
+ height: 2rem;
+ border: 3px solid var(--neutral-300);
+ border-top: 3px solid var(--brand-primary);
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ margin: 0 auto 1rem;
}
-.tableRow {
- cursor: pointer;
- transition: background-color 0.2s ease;
-}
-
-.tableRow:hover {
- background-color: var(--neutral-50);
-}
-
-.nameColumn {
- font-weight: 500;
- color: var(--text-primary);
- min-width: 200px;
+@keyframes spin {
+ to { transform: rotate(360deg); }
}
+/* ===== Empty State ===== */
.emptyState {
text-align: center;
- padding: 3rem 2rem;
+ padding: 3rem 1rem;
color: var(--text-secondary);
}
.emptyStateTitle {
font-size: 1.125rem;
+ font-weight: 700;
+ color: var(--neutral-800);
margin-bottom: 0.5rem;
- color: var(--text-primary);
- font-weight: 600;
}
.emptyStateText {
@@ -172,327 +98,514 @@
color: var(--text-secondary);
}
-.spinner {
- width: 2rem;
- height: 2rem;
- border: 3px solid var(--neutral-300);
- border-top: 3px solid #43a0a7;
+/* ===== Type List ===== */
+.typeList {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ border: 1px solid var(--border-light);
+ border-radius: 0.75rem;
+ background: var(--white);
+ overflow: hidden;
+}
+
+.typeItem {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 1rem;
+ border-bottom: 1px solid var(--border-light);
+ cursor: pointer;
+ transition: all 0.15s ease;
+ min-height: 60px;
+}
+
+.typeItem:last-child {
+ border-bottom: none;
+}
+
+.typeItem:hover {
+ background: rgba(79, 70, 229, 0.05);
+ transform: translateX(4px);
+}
+
+.typeInfo {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ flex: 1;
+ min-width: 0;
+}
+
+.typeNumber {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--brand-gradient);
+ color: var(--text-on-brand);
border-radius: 50%;
- animation: spin 1s linear infinite;
- margin: 0 auto 1rem;
+ font-size: 0.75rem;
+ font-weight: 700;
+ flex-shrink: 0;
+ box-shadow: 0 2px 4px rgba(41, 97, 235, 0.2);
}
-@keyframes spin {
- 0% {
- transform: rotate(0deg);
+.typeDetails {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ min-width: 0;
+}
+
+.typeName {
+ font-weight: 700;
+ color: var(--neutral-800);
+ font-size: 0.95rem;
+ line-height: 1.2;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.typeCount {
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+ font-style: italic;
+}
+
+.typeArrow {
+ font-size: 0.875rem;
+ color: var(--text-secondary);
+ font-weight: 600;
+ flex-shrink: 0;
+}
+
+/* ===== Entry Section ===== */
+.entrySection {
+ animation: slideIn 0.3s ease-out;
+}
+
+@keyframes slideIn {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
}
- 100% {
- transform: rotate(360deg);
+ to {
+ opacity: 1;
+ transform: translateY(0);
}
}
-/* Custom Scrollbar */
-.tableContainer::-webkit-scrollbar {
+.backButton {
+ background: var(--neutral-600);
+ color: var(--text-on-brand);
+ border: none;
+ padding: 0.75rem 1.25rem;
+ border-radius: 0.6rem;
+ cursor: pointer;
+ font-size: 0.875rem;
+ font-weight: 700;
+ margin-bottom: 1.5rem;
+ transition: all 0.15s ease;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ box-shadow: 0 2px 4px rgba(71, 85, 105, 0.2);
+}
+
+.backButton:hover {
+ background: var(--neutral-700);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 8px rgba(71, 85, 105, 0.3);
+}
+
+.entryTitle {
+ margin: 0 0 1rem 0;
+ font-size: clamp(1rem, 0.9vw + 0.7rem, 1.25rem);
+ font-weight: 700;
+ color: var(--neutral-800);
+}
+
+/* ===== Entry List ===== */
+.entryList {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ border: 1px solid var(--border-light);
+ border-radius: 0.75rem;
+ background: var(--white);
+ max-height: 400px;
+ overflow-y: auto;
+}
+
+.entryItem {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 1rem;
+ border-bottom: 1px solid var(--border-light);
+ cursor: pointer;
+ transition: all 0.15s ease;
+ min-height: 60px;
+}
+
+.entryItem:last-child {
+ border-bottom: none;
+}
+
+.entryItem:hover {
+ background: rgba(41, 97, 235, 0.05);
+ transform: translateX(4px);
+}
+
+.entryInfo {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ flex: 1;
+ min-width: 0;
+}
+
+.entryNumber {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--brand-gradient);
+ color: var(--text-on-brand);
+ border-radius: 50%;
+ font-size: 0.75rem;
+ font-weight: 700;
+ flex-shrink: 0;
+ box-shadow: 0 2px 4px rgba(14, 165, 233, 0.2);
+}
+
+.entryDetails {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ min-width: 0;
+}
+
+.entryName {
+ font-weight: 700;
+ color: var(--neutral-800);
+ font-size: 0.9rem;
+ line-height: 1.2;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.entryHint {
+ font-size: 0.75rem;
+ color: var(--text-secondary);
+ font-style: italic;
+}
+
+.entryArrow {
+ font-size: 0.875rem;
+ color: var(--text-secondary);
+ font-weight: 600;
+ flex-shrink: 0;
+}
+
+/* ===== Scrollbar Styling ===== */
+.entryList::-webkit-scrollbar {
width: 8px;
- height: 8px;
}
-.tableContainer::-webkit-scrollbar-track {
+.entryList::-webkit-scrollbar-track {
background: var(--neutral-100);
border-radius: 4px;
}
-.tableContainer::-webkit-scrollbar-thumb {
- background: #43a0a7;
+.entryList::-webkit-scrollbar-thumb {
+ background: var(--neutral-300);
border-radius: 4px;
transition: background 0.2s ease;
}
-.tableContainer::-webkit-scrollbar-thumb:hover {
- background: #306a2f;
+.entryList::-webkit-scrollbar-thumb:hover {
+ background: var(--neutral-400);
}
-.tableContainer::-webkit-scrollbar-corner {
- background: var(--neutral-100);
-}
-
-.tableContainer {
- scrollbar-width: thin;
- scrollbar-color: #43a0a7 var(--neutral-100);
-}
-
-/* Modal Styles - Matching Dashboard Design */
+/* ===== Modal ===== */
.modalOverlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
- background-color: rgba(0, 0, 0, 0.5);
+ background-color: rgba(0, 0, 0, 0.6);
display: flex;
- justify-content: center;
align-items: center;
+ justify-content: center;
z-index: 1000;
backdrop-filter: blur(4px);
+ padding: 1rem;
+ box-sizing: border-box;
+ animation: fadeIn 0.3s ease-out;
}
-.modalContent {
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+.modal {
background: var(--white);
- padding: 2rem;
border-radius: 1rem;
- max-width: 600px;
- width: 90%;
- max-height: 85vh;
- overflow-y: auto;
box-shadow: var(--shadow-lg);
border: 1px solid var(--border-light);
-}
-
-.modalContent h3 {
- margin: 0 0 1.5rem 0;
- font-size: 1.25rem;
- font-weight: 600;
- color: var(--text-primary);
- letter-spacing: -0.025em;
- padding-bottom: 1rem;
- border-bottom: 1px solid var(--border-light);
-}
-
-.detailTable {
width: 100%;
- border-collapse: collapse;
- margin-bottom: 1.5rem;
- font-size: 0.875rem;
- text-align: left;
+ max-width: 900px;
+ max-height: 85vh;
+ overflow: hidden;
+ animation: modalSlideIn 0.3s ease-out;
+ display: flex;
+ flex-direction: column;
}
-.detailTable tr:nth-child(even) {
- background-color: var(--neutral-50);
+@keyframes modalSlideIn {
+ from {
+ opacity: 0;
+ transform: scale(0.95);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
}
-.detailTable td {
- padding: 0.75rem;
+.modalHeader {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1.5rem 1.5rem 0 1.5rem;
border-bottom: 1px solid var(--border-light);
- vertical-align: top;
+ padding-bottom: 1rem;
+ margin-bottom: 1.5rem;
+ flex-shrink: 0;
}
-.detailTable td:first-child {
- font-weight: 600;
- color: var(--text-secondary);
- width: 35%;
- text-transform: uppercase;
- font-size: 0.75rem;
- letter-spacing: 0.05em;
-}
-
-.detailTable td:last-child {
- color: var(--text-primary);
- font-weight: 500;
+.modalTitle {
+ font-size: clamp(1.125rem, 1vw + 0.8rem, 1.375rem);
+ font-weight: 800;
+ color: var(--neutral-800);
+ margin: 0;
+ letter-spacing: -0.015em;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ padding-right: 1rem;
}
.closeButton {
- background-color: #43a0a7;
- color: var(--text-light);
+ background: #ef4444;
+ color: var(--text-on-brand);
border: none;
- padding: 0.75rem 1.5rem;
- border-radius: 0.5rem;
+ border-radius: 50%;
+ width: 36px;
+ height: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
cursor: pointer;
- font-size: 0.875rem;
- font-weight: 600;
- width: 100%;
- transition: all 0.2s ease;
- letter-spacing: 0.025em;
+ font-size: 1.25rem;
+ font-weight: 700;
+ transition: all 0.15s ease;
+ flex-shrink: 0;
+ box-shadow: 0 2px 4px rgba(239, 68, 68, 0.2);
}
.closeButton:hover {
- background-color: #dc2626;
- transform: translateY(-1px);
- box-shadow: var(--shadow-md);
+ background: #dc2626;
+ transform: scale(1.05);
+ box-shadow: 0 4px 8px rgba(239, 68, 68, 0.3);
}
-.closeButton:active {
- transform: translateY(0);
+.modalContent {
+ padding: 0 1.5rem 1.5rem 1.5rem;
+ overflow-y: auto;
+ flex: 1;
}
-/* Responsive Design */
-@media (max-width: 768px) {
- .fileListSection {
- padding: 1.5rem;
- margin: 1rem;
- max-width: 100%;
- height: auto;
- max-height: 70vh;
+.detailGrid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 1rem;
+}
+
+.detailItem {
+ display: flex;
+ flex-direction: column;
+ gap: 0.375rem;
+}
+
+.detailLabel {
+ font-size: 0.75rem;
+ font-weight: 700;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.detailValue {
+ font-size: 0.875rem;
+ color: var(--neutral-800);
+ font-weight: 600;
+ padding: 0.75rem;
+ background: var(--neutral-50);
+ border-radius: 0.5rem;
+ border: 1px solid var(--border-light);
+ min-height: 44px;
+ display: flex;
+ align-items: center;
+ word-break: break-word;
+ overflow-wrap: break-word;
+}
+
+/* ===== Responsive Design ===== */
+
+/* Mobile First - Already defined above */
+
+/* Tablet */
+@media (min-width: 768px) {
+ .container {
+ padding: 2rem;
+ margin: 2rem auto;
+ max-width: 1200px;
}
- .fileListHeader {
- flex-direction: column;
- align-items: flex-start;
- gap: 0.75rem;
+ .typeItem,
+ .entryItem {
+ padding: 1.25rem;
+ min-height: 70px;
}
- .fileListTitle {
- font-size: 1.125rem;
- }
-
- /* Mobile: Show only NIK and Name columns */
- .fileTable {
- min-width: 100%;
- }
-
- .fileTable th:not(:nth-child(2)):not(:nth-child(3)),
- .fileTable td:not(:nth-child(2)):not(:nth-child(3)) {
- display: none;
- }
-
- .fileTable th,
- .fileTable td {
- padding: 0.75rem 0.5rem;
+ .typeNumber,
+ .entryNumber {
+ width: 36px;
+ height: 36px;
font-size: 0.875rem;
}
- .fileTable th:nth-child(2) {
- width: 40%;
+ .typeName,
+ .entryName {
+ font-size: 1rem;
}
- .fileTable th:nth-child(3) {
- width: 60%;
+ .modalHeader {
+ padding: 2rem 2rem 0 2rem;
+ margin-bottom: 2rem;
}
- .nameColumn {
- min-width: unset;
- }
-
- /* Modal responsive */
.modalContent {
- padding: 1.5rem;
- width: 95%;
- max-height: 90vh;
- border-radius: 0.75rem;
- }
-
- .modalContent h3 {
- font-size: 1.125rem;
- }
-
- .detailTable {
- font-size: 0.8125rem;
- }
-
- .detailTable td {
- padding: 0.625rem 0.5rem;
- }
-
- .detailTable td:first-child {
- width: 40%;
+ padding: 0 2rem 2rem 2rem;
}
}
+/* Desktop */
+@media (min-width: 1024px) {
+ .container {
+ padding: 2.5rem;
+ margin: 2.5rem auto;
+ }
+
+ .detailGrid {
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ }
+
+ .entryList {
+ max-height: 500px;
+ }
+}
+
+/* Small Mobile */
@media (max-width: 480px) {
- .fileListSection {
+ .container {
padding: 1rem;
margin: 0.5rem;
border-radius: 0.75rem;
}
- .fileListTitle {
- font-size: 1rem;
+ .typeItem,
+ .entryItem {
+ padding: 0.75rem;
+ min-height: 56px;
}
- .fileCount {
- font-size: 0.6rem;
- padding: 0.25rem 0.5rem;
+ .typeNumber,
+ .entryNumber {
+ width: 28px;
+ height: 28px;
+ font-size: 0.7rem;
}
- .fileTable th,
- .fileTable td {
- padding: 0.5rem 0.375rem;
- font-size: 0.8125rem;
+ .typeName,
+ .entryName {
+ font-size: 0.85rem;
}
- .modalContent {
- padding: 1rem;
- width: 98%;
- border-radius: 0.5rem;
- }
-
- .modalContent h3 {
- font-size: 1rem;
+ .modalHeader {
+ padding: 1rem 1rem 0 1rem;
margin-bottom: 1rem;
}
- .detailTable {
- font-size: 0.75rem;
+ .modalContent {
+ padding: 0 1rem 1rem 1rem;
}
- .detailTable td {
- padding: 0.5rem 0.375rem;
+ .detailGrid {
+ grid-template-columns: 1fr;
+ gap: 0.75rem;
}
- .closeButton {
- padding: 0.625rem 1rem;
+ .detailValue {
+ padding: 0.625rem;
font-size: 0.8125rem;
}
-}
-
-/* Tablet and Desktop enhancements */
-@media (min-width: 769px) {
- .fileListSection {
- padding: 2.5rem;
- margin: 2.5rem auto;
- }
-
- .fileListTitle {
- font-size: 1.5rem;
- }
-
- .fileTable th,
- .fileTable td {
- padding: 1rem 0.75rem;
- }
-
- .modalContent {
- padding: 2.5rem;
- border-radius: 1rem;
- }
-
- .modalContent h3 {
- font-size: 1.5rem;
- margin-bottom: 2rem;
- }
-
- .detailTable {
- font-size: 0.875rem;
- }
-
- .detailTable td {
- padding: 1rem 0.75rem;
- }
.closeButton {
- padding: 0.875rem 2rem;
- display: block;
+ width: 32px;
+ height: 32px;
+ font-size: 1.1rem;
}
}
-@media (min-width: 1024px) {
- .fileListSection {
- padding: 3rem;
- margin: 3rem auto;
+/* Prevent text selection on interactive elements */
+.typeItem,
+.entryItem,
+.backButton,
+.closeButton {
+ user-select: none;
+}
+
+/* Focus styles for accessibility */
+.typeItem:focus,
+.entryItem:focus,
+.backButton:focus,
+.closeButton:focus {
+ outline: none;
+ box-shadow: var(--focus-ring);
+}
+
+/* High contrast mode support */
+@media (prefers-contrast: high) {
+ .typeNumber,
+ .entryNumber {
+ border: 2px solid var(--text-primary);
}
-}
-
-.downloadButton {
- background-color: #164665;
- color: white;
- border: none;
- padding: 0.25rem 0.5rem;
- border-radius: 1rem;
- cursor: pointer;
- font-weight: bold;
- font-size: 0.6rem;
- transition: background-color 0.3s ease;
-}
-
-.downloadButton:hover {
- background-color: #008fc4;
-}
+
+ .modal {
+ border: 2px solid var(--text-primary);
+ }
+}
\ No newline at end of file
diff --git a/src/KTPScanner.js b/src/KTPScanner.js
index c0d5229..1a8675d 100644
--- a/src/KTPScanner.js
+++ b/src/KTPScanner.js
@@ -3,129 +3,150 @@ import { useNavigate } from "react-router-dom";
import PaginatedFormEditable from "./PaginatedFormEditable";
import Modal from "./Modal";
import Expetation from "./Expetation";
+import LiquidGlassHeader from "./components/Header";
+import { Loader2 } from "lucide-react";
+
+
+/**
+ * Perubahan utama:
+ * 1) Tambah komponen
yang muncul saat `loading === true` di SEMUA step
+ * (kamera maupun upload).
+ * 2) `handleManualUpload` sekarang memanggil `setLoading(true)` SEBELUM membaca file, agar
+ * animasi langsung tampil begitu user memilih file.
+ * 3) Tombol-tombol dinonaktifkan ketika loading untuk mencegah double action.
+ * 4) Tambah teks status akesibel (aria-live) agar ramah pembaca layar.
+ */
const spinnerStyle = `
-@keyframes spin { 0% { transform: rotate(0deg);} 100% { transform: rotate(360deg);} }
+@keyframes spin {
+ 0% { transform: rotate(0deg);}
+ 100% { transform: rotate(360deg);}
+}
+@keyframes dots {
+ 0% { content: "."; }
+ 33% { content: ".."; }
+ 66% { content: "..."; }
+ 100% { content: "."; }
+}
+.loading-dots::after { content: "."; animation: dots 1.2s infinite steps(3,end); }
`;
-const ctaBtn = {
- padding: 10,
- backgroundColor: "#ef4444",
- borderRadius: 15,
- color: "white",
- fontWeight: "bold",
- cursor: "pointer",
- marginBottom: "10px",
-};
-
const styles = {
- dashboardHeader: {
- backgroundColor: "var(--white)",
- color: "var(--text-primary)",
- padding: "1rem 1.5rem",
- display: "flex",
- justifyContent: "space-between",
- alignItems: "center",
- boxShadow: "var(--shadow-sm)",
- borderBottom: "3px solid #43a0a7",
- position: "sticky",
- top: 0,
- zIndex: 50,
- backdropFilter: "blur(8px)",
- },
- logoAndTitle: { display: "flex", alignItems: "center", gap: "0.75rem", flexShrink: 0 },
- logo: {
- width: "2.5rem",
- height: "2.5rem",
- borderRadius: "0.75rem",
- marginRight: "0.75rem",
- objectFit: "cover",
- },
- h1: {
- margin: "2px",
- fontSize: "1.5rem",
- fontWeight: "700",
- color: "#43a0a7",
- letterSpacing: "-0.025em",
- },
- dropdownContainer: {
- position: "relative",
- display: "flex",
- alignItems: "center",
- gap: "0.75rem",
- flexShrink: 0,
- },
- dropdownToggle: {
- backgroundColor: "#f5f5f5",
- color: "#0f172a",
- border: "1px solid #e2e8f0",
- padding: "0.5rem",
- borderRadius: "0.5rem",
+ sourceButton: {
+ backgroundColor: "#5856d6",
+ color: "white",
+ padding: "16px 24px",
+ borderRadius: "12px",
+ border: "none",
+ fontSize: "16px",
+ fontWeight: "600",
cursor: "pointer",
- fontSize: "1rem",
- minWidth: "2.5rem",
- height: "2.5rem",
- display: "flex",
- alignItems: "center",
- justifyContent: "center",
+ margin: "8px 0",
+ width: "200px",
+ textAlign: "center",
+ transition: "opacity .2s ease, transform .05s ease",
},
- dropdownMenu: {
- position: "absolute",
- top: "calc(100% + 0.5rem)",
+ sourceButtonText: {
+ color: "#666",
+ fontSize: "16px",
+ margin: "16px 0",
+ fontWeight: "500",
+ },
+ cameraContainer: {
+ position: "fixed",
+ top: 0,
+ left: 0,
right: 0,
- backgroundColor: "white",
- borderRadius: "0.75rem",
- boxShadow:
- "0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1)",
- border: "1px solid #e2e8f0",
- zIndex: 10,
+ bottom: 0,
+ backgroundColor: "black",
+ zIndex: 1000,
+ overflow: "hidden",
display: "flex",
flexDirection: "column",
- minWidth: "10rem",
- overflow: "hidden",
- padding: "0.5rem",
- marginTop: "0.5rem",
- },
- dropdownItem: {
- display: "block",
- width: "100%",
- padding: "0.75rem 1rem",
- border: "none",
- backgroundColor: "transparent",
- textAlign: "left",
- cursor: "pointer",
- fontSize: "0.875rem",
- color: "#0f172a",
- transition: "background-color 0.2s ease",
- borderRadius: "0.5rem",
- marginBottom: "0.125rem",
},
backButton: {
- backgroundColor: "#6c757d",
+ position: "absolute",
+ top: "20px",
+ left: "20px",
+ backgroundColor: "rgba(255, 255, 255, 0.2)",
color: "white",
- padding: "10px 15px",
- borderRadius: "8px",
border: "none",
- fontSize: "14px",
- fontWeight: "bold",
+ borderRadius: "50%",
+ width: "40px",
+ height: "40px",
+ fontSize: "18px",
cursor: "pointer",
- marginBottom: "15px",
- width: "100%",
- },
- spinnerContainer: {
+ zIndex: 1001,
display: "flex",
- justifyContent: "center",
alignItems: "center",
- height: "100px",
+ justifyContent: "center",
+ backdropFilter: "blur(10px)",
+ boxShadow: "0 2px 8px rgba(0,0,0,0.3)",
+ },
+ captureButton: {
+ position: "absolute",
+ bottom: "30px",
+ left: "50%",
+ transform: "translateX(-50%)",
+ backgroundColor: "white",
+ border: "3px solid rgba(255, 255, 255, 0.3)",
+ borderRadius: "50%",
+ width: "70px",
+ height: "70px",
+ fontSize: "24px",
+ cursor: "pointer",
+ zIndex: 1001,
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ boxShadow: "0 4px 12px rgba(0,0,0,0.4)",
+ transition: "transform 0.1s ease",
+ },
+ canvas: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ width: '100%',
+ height: '100%',
+ zIndex: 2,
+ objectFit: 'cover',
+ },
+ guideText: {
+ position: "absolute",
+ top: "80px",
+ left: "50%",
+ transform: "translateX(-50%)",
+ color: "white",
+ fontSize: "16px",
+ fontWeight: "500",
+ zIndex: 1001,
+ textAlign: "center",
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
+ padding: "12px 20px",
+ borderRadius: "20px",
+ backdropFilter: "blur(10px)",
+ maxWidth: "90%",
},
spinner: {
border: "4px solid #f3f3f3",
borderTop: "4px solid #429241",
borderRadius: "50%",
- width: "40px",
- height: "40px",
+ width: "44px",
+ height: "44px",
animation: "spin 1s linear infinite",
},
+ ctaBtn: {
+ padding: "16px 24px",
+ backgroundColor: "#5856d6",
+ borderRadius: "12px",
+ color: "white",
+ fontWeight: "600",
+ cursor: "pointer",
+ textAlign: "center",
+ margin: "8px auto",
+ width: "200px",
+ fontSize: "16px",
+ },
};
/* ============================
@@ -133,11 +154,9 @@ const styles = {
============================ */
const getCleanToken = () => {
let raw = localStorage.getItem("token") || "";
- try { raw = JSON.parse(raw); } catch {}
+ try { raw = JSON.parse(raw); } catch { }
return String(raw).replace(/^"+|"+$/g, "");
};
-
-// Baca org dari localStorage: pake 'selected_organization' dulu, fallback 'select_organization'
const getSelectedOrganization = () => {
let raw =
localStorage.getItem("selected_organization") ??
@@ -145,16 +164,18 @@ const getSelectedOrganization = () => {
if (!raw) return null;
try { return JSON.parse(raw); } catch { return raw; }
};
-
-// Ambil organization_id aktif (string / dari object)
const getActiveOrgId = () => {
const sel = getSelectedOrganization();
if (!sel) return "";
if (typeof sel === "object" && sel?.organization_id) return String(sel.organization_id);
return String(sel);
};
-
-// Header umum (JANGAN set Content-Type untuk FormData)
+const getActiveNamaType = (selectedDocumentType) => {
+ if (!selectedDocumentType) return "";
+ if (selectedDocumentType.id) return String(selectedDocumentType.id);
+ if (selectedDocumentType.display_name) return String(selectedDocumentType.display_name);
+ return "";
+};
const authHeaders = ({ isJson = false } = {}) => {
const token = getCleanToken();
const orgId = getActiveOrgId();
@@ -165,40 +186,65 @@ const authHeaders = ({ isJson = false } = {}) => {
return isJson ? { "Content-Type": "application/json", ...base } : base;
};
-const KTPScanner = () => {
- const [isMenuOpen, setIsMenuOpen] = useState(false);
- const menuRef = useRef(null);
- const navigate = useNavigate();
+/* ============================
+ KOMPPN: LoadingOverlay
+============================ */
+const LoadingOverlay = ({ message = "Memproses..." }) => (
+
+);
+
+
+/* ============================
+ MAIN COMPONENT
+============================ */
+const KTPScanner = () => {
+ const navigate = useNavigate();
const handleLogout = () => {
localStorage.removeItem("token");
localStorage.removeItem("user");
window.location.reload();
};
+ // refs
const videoRef = useRef(null);
const canvasRef = useRef(null);
const hiddenCanvasRef = useRef(null);
+ const fileInputRef = useRef(null);
+ const freezeFrameRef = useRef(null);
+ const rectRef = useRef({ x: 0, y: 0, width: 0, height: 0, radius: 20 });
+
+ // state
+ const [step, setStep] = useState("document"); // "document" | "source" | "camera" | "preview"
const [capturedImage, setCapturedImage] = useState(null);
const [fileTemp, setFileTemp] = useState(null);
const [isFreeze, setIsFreeze] = useState(false);
- const freezeFrameRef = useRef(null);
-
const [loading, setLoading] = useState(false);
- const [showDocumentSelection, setShowDocumentSelection] = useState(true);
- // selectedDocumentType menyimpan OBJEK dokumen (dari Expetation), termasuk expectation
const [selectedDocumentType, setSelectedDocumentType] = useState(null);
const [cameraInitialized, setCameraInitialized] = useState(false);
-
const [isScanned, setIsScanned] = useState(false);
const [showSuccessMessage, setShowSuccessMessage] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
- const fileInputRef = useRef(null);
- const triggerFileSelect = () => fileInputRef.current?.click();
-
- const rectRef = useRef({ x: 0, y: 0, width: 0, height: 0, radius: 20 });
-
+ /* ============================
+ CAMERA FUNCTIONS - PORTRAIT OPTIMIZED
+ ============================ */
const drawRoundedRect = (ctx, x, y, width, height, radius) => {
ctx.beginPath();
ctx.moveTo(x + radius, y);
@@ -211,14 +257,51 @@ const KTPScanner = () => {
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
- ctx.strokeStyle = "red";
+ ctx.strokeStyle = "#fff";
ctx.lineWidth = 3;
ctx.stroke();
+
+ const cornerSize = 25;
+ ctx.strokeStyle = "#00ff88";
+ ctx.lineWidth = 4;
+
+ // Top-left
+ ctx.beginPath();
+ ctx.moveTo(x + radius, y);
+ ctx.lineTo(x + radius + cornerSize, y);
+ ctx.moveTo(x, y + radius);
+ ctx.lineTo(x, y + radius + cornerSize);
+ ctx.stroke();
+
+ // Top-right
+ ctx.beginPath();
+ ctx.moveTo(x + width - radius - cornerSize, y);
+ ctx.lineTo(x + width - radius, y);
+ ctx.moveTo(x + width, y + radius);
+ ctx.lineTo(x + width, y + radius + cornerSize);
+ ctx.stroke();
+
+ // Bottom-left
+ ctx.beginPath();
+ ctx.moveTo(x, y + height - radius - cornerSize);
+ ctx.lineTo(x, y + height - radius);
+ ctx.moveTo(x + radius, y + height);
+ ctx.lineTo(x + radius + cornerSize, y + height);
+ ctx.stroke();
+
+ // Bottom-right
+ ctx.beginPath();
+ ctx.moveTo(x + width, y + height - radius - cornerSize);
+ ctx.lineTo(x + width, y + height - radius);
+ ctx.moveTo(x + width - radius - cornerSize, y + height);
+ ctx.lineTo(x + width - radius, y + height);
+ ctx.stroke();
};
const fillOutsideRect = (ctx, rect, canvasWidth, canvasHeight) => {
ctx.save();
const { x, y, width, height, radius } = rect;
+
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
@@ -230,52 +313,112 @@ const KTPScanner = () => {
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
+
ctx.rect(0, 0, canvasWidth, canvasHeight);
- ctx.fillStyle = "rgba(173, 173, 173, 1)";
+ ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
ctx.fill("evenodd");
ctx.restore();
};
const initializeCamera = async () => {
try {
- const stream = await navigator.mediaDevices.getUserMedia({
- video: { facingMode: { ideal: "environment" } },
+ if (typeof window !== "undefined" && window.screen?.orientation?.lock) {
+ try { await window.screen.orientation.lock("potrait") ; } catch (e) {}
+ }
+
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ const constraints = {
+ video: {
+ facingMode: { ideal: "environment" },
+ width: { ideal: 1280 },
+ height: { ideal: 720 },
+ frameRate: { ideal: 30, min: 15 }
+ },
audio: false,
- });
+ };
+
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
if (videoRef.current) {
videoRef.current.srcObject = stream;
videoRef.current.onloadedmetadata = () => {
videoRef.current.play();
+
+ setIsFreeze(false);
+ freezeFrameRef.current = null;
+
const video = videoRef.current;
const canvas = canvasRef.current;
const hiddenCanvas = hiddenCanvasRef.current;
const ctx = canvas.getContext("2d");
- canvas.width = video.videoWidth;
- canvas.height = video.videoHeight;
- canvas.style.maxWidth = "100%";
- canvas.style.height = "auto";
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
+
+ canvas.width = viewportWidth * pixelRatio;
+ canvas.height = viewportHeight * pixelRatio;
+ canvas.style.width = viewportWidth + 'px';
+ canvas.style.height = viewportHeight + 'px';
+
+ ctx.scale(pixelRatio, pixelRatio);
hiddenCanvas.width = video.videoWidth;
hiddenCanvas.height = video.videoHeight;
- const rectWidth = canvas.width * 0.9;
- const rectHeight = (53.98 / 85.6) * rectWidth;
- const rectX = (canvas.width - rectWidth) / 2;
- const rectY = (canvas.height - rectHeight) / 2;
+ const margin = 40;
+ const topBottomMargin = 120;
+ const availableWidth = viewportWidth - (margin * 2);
+ const availableHeight = viewportHeight - (topBottomMargin * 2);
+ const portraitRatio = 0.63;
+ let rectWidth, rectHeight;
+ rectWidth = Math.min(availableWidth * 0.85, 280);
+ rectHeight = rectWidth / portraitRatio;
+ if (rectHeight > availableHeight) {
+ rectHeight = availableHeight;
+ rectWidth = rectHeight * portraitRatio;
+ }
+ rectRef.current = {
+ x: (viewportWidth - rectWidth) / 2,
+ y: (viewportHeight - rectHeight) / 2,
+ width: rectWidth,
+ height: rectHeight,
+ radius: 20,
+ };
- rectRef.current = { x: rectX, y: rectY, width: rectWidth, height: rectHeight, radius: 20 };
+ const drawLoop = () => {
+ if (video.readyState >= 2) {
+ ctx.clearRect(0, 0, viewportWidth, viewportHeight);
- const drawToCanvas = () => {
- if (video.readyState === 4) {
- ctx.clearRect(0, 0, canvas.width, canvas.height);
if (isFreeze && freezeFrameRef.current) {
- ctx.putImageData(freezeFrameRef.current, 0, 0);
+ const tempCanvas = document.createElement('canvas');
+ tempCanvas.width = freezeFrameRef.current.width;
+ tempCanvas.height = freezeFrameRef.current.height;
+ tempCanvas.getContext('2d').putImageData(freezeFrameRef.current, 0, 0);
+ ctx.drawImage(tempCanvas, 0, 0, viewportWidth, viewportHeight);
} else {
- ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
+ const videoAspect = video.videoWidth / video.videoHeight;
+ const screenAspect = viewportWidth / viewportHeight;
+ let drawWidth, drawHeight, offsetX, offsetY;
+ if (videoAspect > screenAspect) {
+ drawHeight = viewportHeight;
+ drawWidth = drawHeight * videoAspect;
+ offsetX = (viewportWidth - drawWidth) / 2;
+ offsetY = 0;
+ } else {
+ drawWidth = viewportWidth;
+ drawHeight = drawWidth / videoAspect;
+ offsetX = 0;
+ offsetY = (viewportHeight - drawHeight) / 2;
+ }
+ ctx.drawImage(video, offsetX, offsetY, drawWidth, drawHeight);
}
+
+ if (!isFreeze) {
+ fillOutsideRect(ctx, rectRef.current, viewportWidth, viewportHeight);
+ }
+
drawRoundedRect(
ctx,
rectRef.current.x,
@@ -284,98 +427,100 @@ const KTPScanner = () => {
rectRef.current.height,
rectRef.current.radius
);
- if (isFreeze) fillOutsideRect(ctx, rectRef.current, canvas.width, canvas.height);
}
- if (!showDocumentSelection) requestAnimationFrame(drawToCanvas);
+ requestAnimationFrame(drawLoop);
};
- drawToCanvas();
+ drawLoop();
setCameraInitialized(true);
};
}
} catch (err) {
console.error("Gagal mendapatkan kamera:", err);
+ alert("Tidak dapat mengakses kamera. Pastikan browser memiliki izin kamera.");
}
};
- useEffect(() => {
- const handleClickOutside = (event) => {
- if (menuRef.current && !menuRef.current.contains(event.target)) {
- setIsMenuOpen(false);
- }
- };
- document.addEventListener("mousedown", handleClickOutside);
- return () => document.removeEventListener("mousedown", handleClickOutside);
- }, []);
-
- useEffect(() => {
- if (cameraInitialized) {
- const video = videoRef.current;
- const canvas = canvasRef.current;
- const ctx = canvas.getContext("2d");
-
- const drawToCanvas = () => {
- if (video && video.readyState === 4) {
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- if (isFreeze && freezeFrameRef.current) {
- ctx.putImageData(freezeFrameRef.current, 0, 0);
- } else {
- ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
- }
- drawRoundedRect(
- ctx,
- rectRef.current.x,
- rectRef.current.y,
- rectRef.current.width,
- rectRef.current.height,
- rectRef.current.radius
- );
- if (isFreeze) fillOutsideRect(ctx, rectRef.current, canvas.width, canvas.height);
- }
- if (!showDocumentSelection) requestAnimationFrame(drawToCanvas);
- };
-
- if (!showDocumentSelection) drawToCanvas();
- }
- }, [isFreeze, cameraInitialized, showDocumentSelection]);
-
+ /* ============================
+ CAPTURE & SCAN - PORTRAIT OPTIMIZED
+ ============================ */
const shootImage = async () => {
const video = videoRef.current;
+ const canvas = canvasRef.current;
const { x, y, width, height } = rectRef.current;
const hiddenCanvas = hiddenCanvasRef.current;
const hiddenCtx = hiddenCanvas.getContext("2d");
- const visibleCtx = canvasRef.current.getContext("2d");
+ const visibleCtx = canvas.getContext("2d");
- freezeFrameRef.current = visibleCtx.getImageData(0, 0, canvasRef.current.width, canvasRef.current.height);
- setIsFreeze(true);
+ // Munculkan loading segera ketika user menekan tombol tangkap
setLoading(true);
+ const pixelRatio = window.devicePixelRatio || 1;
+ freezeFrameRef.current = visibleCtx.getImageData(0, 0, canvas.width, canvas.height);
+ setIsFreeze(true);
+ video.pause();
+
hiddenCtx.drawImage(video, 0, 0, hiddenCanvas.width, hiddenCanvas.height);
- const cropCanvas = document.createElement("canvas");
- cropCanvas.width = Math.floor(width);
- cropCanvas.height = Math.floor(height);
- const cropCtx = cropCanvas.getContext("2d");
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
+ const videoAspect = video.videoWidth / video.videoHeight;
+ const screenAspect = viewportWidth / viewportHeight;
+
+ let videoDisplayWidth, videoDisplayHeight, videoOffsetX, videoOffsetY;
+
+ if (videoAspect > screenAspect) {
+ videoDisplayHeight = viewportHeight;
+ videoDisplayWidth = videoDisplayHeight * videoAspect;
+ videoOffsetX = (viewportWidth - videoDisplayWidth) / 2;
+ videoOffsetY = 0;
+ } else {
+ videoDisplayWidth = viewportWidth;
+ videoDisplayHeight = videoDisplayWidth / videoAspect;
+ videoOffsetX = 0;
+ videoOffsetY = (viewportHeight - videoDisplayHeight) / 2;
+ }
+
+ const scaleX = hiddenCanvas.width / videoDisplayWidth;
+ const scaleY = hiddenCanvas.height / videoDisplayHeight;
+
+ const cropX = (x - videoOffsetX) * scaleX;
+ const cropY = (y - videoOffsetY) * scaleY;
+ const cropWidth = width * scaleX;
+ const cropHeight = height * scaleY;
+
+ const finalCropX = Math.max(0, Math.min(cropX, hiddenCanvas.width - cropWidth));
+ const finalCropY = Math.max(0, Math.min(cropY, hiddenCanvas.height - cropHeight));
+ const finalCropWidth = Math.min(cropWidth, hiddenCanvas.width - finalCropX);
+ const finalCropHeight = Math.min(cropHeight, hiddenCanvas.height - finalCropY);
+
+ const cropCanvas = document.createElement("canvas");
+ cropCanvas.width = Math.floor(finalCropWidth);
+ cropCanvas.height = Math.floor(finalCropHeight);
+
+ const cropCtx = cropCanvas.getContext("2d");
cropCtx.drawImage(
hiddenCanvas,
- Math.floor(x),
- Math.floor(y),
- Math.floor(width),
- Math.floor(height),
+ Math.floor(finalCropX),
+ Math.floor(finalCropY),
+ Math.floor(finalCropWidth),
+ Math.floor(finalCropHeight),
0,
0,
- Math.floor(width),
- Math.floor(height)
+ Math.floor(finalCropWidth),
+ Math.floor(finalCropHeight)
);
- const imageDataUrl = cropCanvas.toDataURL("image/png", 1.0);
- setCapturedImage(imageDataUrl);
+ const imageData = cropCanvas.toDataURL("image/jpeg", 0.9);
+ setCapturedImage(imageData);
+
+
setLoading(false);
};
- function base64ToFile(base64Data, fileName) {
+ const base64ToFile = (base64Data, fileName) => {
const arr = base64Data.split(",");
const mime = arr[0].match(/:(.*?);/)[1];
const bstr = atob(arr[1]);
@@ -383,73 +528,60 @@ const KTPScanner = () => {
const u8arr = new Uint8Array(n);
while (n--) u8arr[n] = bstr.charCodeAt(n);
return new File([u8arr], fileName, { type: mime });
- }
+ };
- // Scan (kirim image + expectation + organization_id)
const ReadImage = async (capturedImage) => {
try {
setLoading(true);
- const token = getCleanToken();
const orgId = getActiveOrgId();
+ const namaType = getActiveNamaType(selectedDocumentType);
const file = base64ToFile(capturedImage, "image.jpg");
-
const formData = new FormData();
formData.append("image", file);
-
- // Kirim expectation (bukan sekadar document_type)
- const expectation = selectedDocumentType?.expectation || {};
- formData.append("expectation", JSON.stringify(expectation));
-
- // (opsional) jika backend masih butuh identifier tipe
- if (selectedDocumentType?.document_type) {
- formData.append("document_type", selectedDocumentType.document_type);
- }
-
- // >>> penting: sertakan organization_id
+ formData.append("expectation", JSON.stringify(selectedDocumentType?.expectation || {}));
if (orgId) formData.append("organization_id", orgId);
-
- const res = await fetch(
- "https://bot.kediritechnopark.com/webhook/solid-data/scan",
- {
- method: "POST",
- headers: authHeaders(), // JANGAN set Content-Type (biar FormData yang atur)
- body: formData,
- }
- );
+ if (namaType) formData.append("data_type_id", namaType);
+ const res = await fetch("https://bot.kediritechnopark.com/webhook/solid-data/scan", {
+ method: "POST",
+ headers: authHeaders(),
+ body: formData,
+ });
+ const data = await res.json();
+ console.log("Scan result:", data);
setLoading(false);
-
- const data = await res.json();
if (data.responseCode === 409) {
setFileTemp({ error: 409 });
- setIsScanned(true);
- return;
+ } else if (!data || Object.keys(data).length === 0) {
+ setFileTemp({});
+ } else {
+ setFileTemp(data);
}
- setFileTemp(data);
setIsScanned(true);
- } catch (error) {
- console.error("Failed to read image:", error);
+ setStep("preview");
+
+ } catch (err) {
+ console.error("Failed to read image:", err);
+ setFileTemp({});
setIsScanned(true);
+ setLoading(false);
+ setStep("preview");
}
};
- // SAVE (tambahkan organization_id)
- const handleSaveTemp = async (verifiedData, documentType) => {
+ const handleSaveTemp = async (verifiedData) => {
try {
setLoading(true);
- const token = getCleanToken();
const orgId = getActiveOrgId();
-
+ const namaType = getActiveNamaType(selectedDocumentType);
const formData = new FormData();
formData.append("data", JSON.stringify(verifiedData));
- formData.append("document_type", documentType || "");
-
- // >>> penting: sertakan organization_id
if (orgId) formData.append("organization_id", orgId);
+ if (namaType) formData.append("data_type_id", namaType);
await fetch("https://bot.kediritechnopark.com/webhook/solid-data/save", {
method: "POST",
- headers: authHeaders(), // Authorization + X-Organization-Id
+ headers: authHeaders(),
body: formData,
});
@@ -462,281 +594,306 @@ const KTPScanner = () => {
setIsFreeze(false);
setIsScanned(false);
setCapturedImage(null);
- }, 3000);
+ setStep("source");
+ }, 2000);
} catch (err) {
- console.error("Gagal menyimpan ke server:", err);
+ console.error("Gagal simpan:", err);
setLoading(false);
}
};
- // DELETE temp (sertakan organization_id)
- const handleDeleteTemp = async () => {
- try {
- const orgId = getActiveOrgId();
- await fetch("https://bot.kediritechnopark.com/webhook/solid-data/delete", {
- method: "POST",
- headers: authHeaders({ isJson: true }),
- body: JSON.stringify({
- fileTemp,
- ...(orgId ? { organization_id: orgId } : {}),
- }),
- });
- setFileTemp(null);
- } catch (err) {
- console.error("Gagal menghapus dari server:", err);
- }
- };
-
+ /* ============================
+ HELPERS
+ ============================ */
const handleManualUpload = async (e) => {
const file = e.target.files[0];
if (!file) return;
+ await new Promise(requestAnimationFrame);
+
+ // TAMPILKAN LOADING SEGERA begitu user memilih file
+ setLoading(true);
const reader = new FileReader();
- reader.onloadend = () => {
- const imageDataUrl = reader.result;
- setCapturedImage(imageDataUrl);
+ reader.onloadend = async () => {
+ setCapturedImage(reader.result);
setIsFreeze(true);
-
- const image = new Image();
- image.onload = async () => {
- const rectWidth = rectRef.current.width;
- const rectHeight = rectRef.current.height;
- const canvas = canvasRef.current;
- const ctx = canvas.getContext("2d");
-
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
-
- drawRoundedRect(
- ctx,
- rectRef.current.x,
- rectRef.current.y,
- rectRef.current.width,
- rectRef.current.height,
- rectRef.current.radius
- );
- fillOutsideRect(ctx, rectRef.current, canvas.width, canvas.height);
-
- freezeFrameRef.current = ctx.getImageData(0, 0, canvas.width, canvas.height);
-
- const cropCanvas = document.createElement("canvas");
- cropCanvas.width = rectWidth;
- cropCanvas.height = rectHeight;
- const cropCtx = cropCanvas.getContext("2d");
-
- cropCtx.drawImage(
- canvas,
- rectRef.current.x,
- rectRef.current.y,
- rectWidth,
- rectHeight,
- 0,
- 0,
- rectWidth,
- rectHeight
- );
- };
- image.src = imageDataUrl;
+ await ReadImage(reader.result);
+ setStep("preview");
+ // ReadImage sudah mengelola setLoading
};
reader.readAsDataURL(file);
};
- const goBackToSelection = () => {
- setShowDocumentSelection(true);
- setSelectedDocumentType(null);
- setCameraInitialized(false);
- setIsFreeze(false);
+ const triggerFileSelect = () => fileInputRef.current?.click();
+ const resetToSource = () => {
setCapturedImage(null);
- setFileTemp(null);
- setIsScanned(false);
- setShowSuccessMessage(false);
-
- if (videoRef.current && videoRef.current.srcObject) {
- const stream = videoRef.current.srcObject;
- const tracks = stream.getTracks();
- tracks.forEach((track) => track.stop());
- videoRef.current.srcObject = null;
- }
- };
-
- const handleHapus = () => {
setFileTemp(null);
setIsFreeze(false);
setIsScanned(false);
- setCapturedImage(null);
- setShowSuccessMessage(false);
- if (videoRef.current && videoRef.current.srcObject) {
- const stream = videoRef.current.srcObject;
- const tracks = stream.getTracks();
- tracks.forEach((track) => track.stop());
- videoRef.current.srcObject = null;
+ setStep("source");
+ if (videoRef.current?.srcObject) {
+ videoRef.current.srcObject.getTracks().forEach((t) => t.stop());
+ }
+ if (typeof window !== "undefined" && window.screen?.orientation?.unlock) {
+ window.screen.orientation.unlock() ;
}
};
+ const handleRetake = () => {
+ // bersihkan hasil freeze/capture dan balik ke live preview kamera
+ setCapturedImage(null);
+ setFileTemp(null);
+ setIsScanned(false);
+ setIsFreeze(false);
+ try {
+ videoRef.current?.play(); // lanjutkan stream kamera
+ } catch {}
+};
- // selection callback from Expetation (menerima OBJEK dokumen)
const handleSelectDocumentType = (doc) => {
setSelectedDocumentType(doc);
- setShowDocumentSelection(false);
- initializeCamera();
+ setStep("source");
};
useEffect(() => {
- const video = videoRef.current;
return () => {
- if (video && video.srcObject) {
- video.srcObject.getTracks().forEach((t) => t.stop());
+ if (videoRef.current?.srcObject) {
+ videoRef.current.srcObject.getTracks().forEach((t) => t.stop());
+ }
+ if (typeof window !== "undefined" && window.screen?.orientation?.unlock) {
+ window.screen.orientation.unlock();
}
};
}, []);
+ /* ============================
+ RENDER
+ ============================ */
return (
-
-
-

-
SOLID
-
DATA
-
+
+
+ {step !== "camera" && (
+
navigate("/dashboard")}
+ />
+ )}
+
+ {/* STEP 1: PILIH DOKUMEN */}
+ {step === "document" && }
+
+ {/* STEP 2: PILIH SUMBER */}
+ {step === "source" && (
+
+
Pilih Sumber Gambar
-
- {isMenuOpen && (
-
-
-
-
- )}
-
-
-
- {showDocumentSelection ? (
-
- ) : (
- <>
-
-
-
-
- {
+ setStep("camera");
+ initializeCamera();
}}
>
-
+
+
Atau
+
+
+
+
+
+
+
+ )}
+
+ {/* STEP 3: KAMERA - PORTRAIT FULL SCREEN */}
+ {step === "camera" && (
+
+
+
+
+
+
+
+ {!isFreeze && !loading && (
+
+ )}
- {showSuccessMessage ? (
-
- Data berhasil disimpan
+ {isFreeze && !loading && !isScanned && (
+
+ {/* 1) Scan */}
+
+
+ {/* 2) Ulangi (retake) — kamera tetap terbuka */}
+
+
+ {/* 3) Hapus & kembali ke pilih sumber */}
+
+
+)}
+
+
+
+ )}
+
+ {/* HASIL SCAN (kamera atau upload) */}
+ {capturedImage && step !== "camera" && (
+
+ {!isScanned ? (
+ <>
+
Memproses Gambar...
+

+ >
+ ) : fileTemp && fileTemp.error !== 409 ? (
+ <>
+

+
+
+
+
- ) : !isFreeze ? (
- <>
-
-
- Ambil Gambar
-
-
- Upload Gambar
-
-
-
handleManualUpload(e)}
- style={{ display: "none" }}
- />
- >
- ) : loading ? (
-
- ) : (
- capturedImage && (!fileTemp || fileTemp.error === undefined) && !isScanned && (
-
-
Tinjau Gambar
-
ReadImage(capturedImage)}>
- Scan
-
-
- Hapus
-
-
- )
- )}
-
- {fileTemp && fileTemp.error !== "409" ? (
-
- handleSaveTemp(data, selectedDocumentType?.document_type || "")
- }
- />
- ) : (
- fileTemp && (
- <>
- KTP Sudah Terdaftar
-
- Hapus
-
- >
- )
- )}
-
- >
+ >
+ ) : fileTemp && (
+ <>
+

+
Dokumen Sudah Terdaftar
+
Hapus
+ >
+ )}
+
)}
{
loading={loading}
fileTemp={fileTemp}
onSave={handleSaveTemp}
- onDelete={handleDeleteTemp}
/>
+
+ {showSuccessMessage && (
+
+ ✅ Data berhasil disimpan!
+
+ )}
+
+ {/* GLOBAL OVERLAY: tampil di SEMUA step ketika loading */}
+ {loading && (
+
+ )}
);
};
diff --git a/src/Login.js b/src/Login.js
index 544f9f9..a6386bd 100644
--- a/src/Login.js
+++ b/src/Login.js
@@ -10,7 +10,7 @@ import styles from "./Login.module.css";
export default function LoginPage({ onLoggedIn }) {
const login = () => {
- const baseUrl = "http://localhost:3001/";
+ const baseUrl = "https://kediritechnopark.com/";
const modal = "product";
const productId = 9;
diff --git a/src/components/Header.js b/src/components/Header.js
index c3ebf36..4239072 100644
--- a/src/components/Header.js
+++ b/src/components/Header.js
@@ -3,11 +3,7 @@ import React from "react";
import styles from "./Header.module.css";
const Header = () => {
- return (
-
- );
+
};
export default Header;
diff --git a/src/components/Sidebar.js b/src/components/Sidebar.js
index d68cf22..f95cbfd 100644
--- a/src/components/Sidebar.js
+++ b/src/components/Sidebar.js
@@ -1,11 +1,14 @@
// Sidebar.js
import React from "react";
+import { Link } from "react-router-dom";
import styles from "./Sidebar.module.css";
const Sidebar = () => {
return (
-
Dashboard
+
+ Dashboard
+