first commit
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
<template>
|
||||
<div class="odontogram-container">
|
||||
<AppsMedicalOdontogramToolbar
|
||||
:currentMode="mode"
|
||||
@modeChange="setMode"
|
||||
@clearAll="clearAll"
|
||||
/>
|
||||
|
||||
<canvas
|
||||
ref="canvas"
|
||||
:width="width"
|
||||
:height="height"
|
||||
class="odontogram-canvas"
|
||||
@mousemove="onMouseMove"
|
||||
@click="onMouseClick"
|
||||
>
|
||||
Browser anda tidak support canvas, silahkan update browser anda.
|
||||
</canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted, watch, defineEmits } from "vue";
|
||||
import {
|
||||
useOdontogram,
|
||||
convertGeom
|
||||
} from "~/composables/apps/medical/useOdontogram";
|
||||
import { useToothRenderer } from "~/composables/apps/medical/useToothRenderer";
|
||||
import { useOdontogramStore } from "~/store/apps/medical/odontogram";
|
||||
|
||||
const emit = defineEmits(["update:geometry"]);
|
||||
|
||||
const canvas = ref<HTMLCanvasElement | null>(null);
|
||||
const width = 1500;
|
||||
const height = 675;
|
||||
|
||||
const store = useOdontogramStore();
|
||||
|
||||
const {
|
||||
odontogramInstance,
|
||||
mode,
|
||||
initialize,
|
||||
setMode,
|
||||
onMouseMove,
|
||||
onMouseClick,
|
||||
geometry,
|
||||
clearAll
|
||||
} = useOdontogram();
|
||||
|
||||
const { renderCondition } = useToothRenderer();
|
||||
|
||||
function initializeGeometryFromConditions() {
|
||||
if (odontogramInstance.value && store.conditions.length > 0) {
|
||||
const newGeometry: Record<string, any[]> = {};
|
||||
const teeth = odontogramInstance.value.teeth;
|
||||
|
||||
// Helper to get polygons for a given surface on a tooth
|
||||
function getPolygonsForSurface(teeth: any, surfaceKey: string) {
|
||||
const polygons = [];
|
||||
for (const key in teeth) {
|
||||
if (key === surfaceKey) {
|
||||
const polygonOpt = {
|
||||
fillStyle: "rgba(55, 55, 55, 0.2)"
|
||||
};
|
||||
const vertices = [];
|
||||
for (const vertexKey in teeth[key]) {
|
||||
vertices.push(teeth[key][vertexKey]);
|
||||
}
|
||||
const pol = new Polygon(vertices, polygonOpt);
|
||||
pol.name = key;
|
||||
polygons.push(pol);
|
||||
}
|
||||
}
|
||||
return polygons;
|
||||
}
|
||||
|
||||
// Build a map from tooth.num to toothKey for quick lookup
|
||||
const toothNumToKeyMap = new Map<string, string>();
|
||||
Object.keys(teeth).forEach((key) => {
|
||||
const tooth = teeth[key];
|
||||
if (tooth && tooth.num) {
|
||||
toothNumToKeyMap.set(tooth.num, key);
|
||||
}
|
||||
});
|
||||
|
||||
for (const condition of store.conditions) {
|
||||
if (!teeth) continue;
|
||||
const toothKey = toothNumToKeyMap.get(String(condition.toothNumber));
|
||||
if (!toothKey) {
|
||||
console.warn(
|
||||
`No tooth key found for toothNumber: ${condition.toothNumber}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// Map short surface codes to full property names in teeth object
|
||||
const surfaceMap: Record<string, string> = {
|
||||
T: "top",
|
||||
R: "right",
|
||||
B: "bottom",
|
||||
L: "left",
|
||||
M: "middle"
|
||||
};
|
||||
let surfaceKey = "middle";
|
||||
if (
|
||||
condition.surface &&
|
||||
["T", "R", "B", "L", "M"].includes(condition.surface)
|
||||
) {
|
||||
surfaceKey = surfaceMap[condition.surface];
|
||||
} else if (condition.position && typeof condition.position === "string") {
|
||||
// Try to infer surface from position string (pos)
|
||||
const parts = condition.position.split("-");
|
||||
if (parts.length > 1) {
|
||||
const surf = parts[1].toUpperCase();
|
||||
if (["T", "R", "B", "L", "M"].includes(surf)) {
|
||||
surfaceKey = surfaceMap[surf];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For AMF, COF, FIS, CARIES modes, use polygons for surface
|
||||
if (
|
||||
condition.mode === 1 || // AMF
|
||||
condition.mode === 2 || // COF
|
||||
condition.mode === 3 || // FIS
|
||||
condition.mode === 10 // CARIES
|
||||
) {
|
||||
const polygons = getPolygonsForSurface(teeth, surfaceKey);
|
||||
if (polygons.length > 0) {
|
||||
if (!newGeometry[toothKey]) {
|
||||
newGeometry[toothKey] = [];
|
||||
}
|
||||
for (const pol of polygons) {
|
||||
pol.pos =
|
||||
condition.toothNumber + "-" + pol.name.charAt(0).toUpperCase();
|
||||
const geom = convertGeom(pol, condition.mode);
|
||||
newGeometry[toothKey].push(geom);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: use tooth rectangle vertices and pos as tooth number
|
||||
const coord = odontogramInstance.value.teeth[toothKey];
|
||||
if (!newGeometry[toothKey]) {
|
||||
newGeometry[toothKey] = [];
|
||||
}
|
||||
if (coord) {
|
||||
// Append start/finish suffix to pos for bridge mode
|
||||
let posValue = condition.toothNumber;
|
||||
if (
|
||||
condition.mode === 18 &&
|
||||
condition.position &&
|
||||
typeof condition.position === "string"
|
||||
) {
|
||||
if (condition.position.toLowerCase().includes("start")) {
|
||||
posValue = condition.toothNumber + "-start";
|
||||
} else if (condition.position.toLowerCase().includes("finish")) {
|
||||
posValue = condition.toothNumber + "-finish";
|
||||
}
|
||||
}
|
||||
const geom = convertGeom(
|
||||
{
|
||||
vertices: [
|
||||
{ x: coord.x1, y: coord.y1 },
|
||||
{ x: coord.x2, y: coord.y2 }
|
||||
],
|
||||
pos: posValue
|
||||
},
|
||||
condition.mode
|
||||
);
|
||||
newGeometry[toothKey].push(geom);
|
||||
}
|
||||
}
|
||||
odontogramInstance.value.geometry = newGeometry;
|
||||
odontogramInstance.value.redraw();
|
||||
console.log("Geometry initialized and canvas redrawn.");
|
||||
} else {
|
||||
console.log("No conditions found or odontogram instance missing.");
|
||||
}
|
||||
console.log("Odontogram instance initialized:", odontogramInstance.value);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (canvas.value) {
|
||||
console.log(
|
||||
"Initializing odontogram canvas with width:",
|
||||
width,
|
||||
"height:",
|
||||
height
|
||||
);
|
||||
initialize(canvas.value, width, height);
|
||||
// Load data synchronously
|
||||
store.loadStoredData();
|
||||
console.log("Data loaded from store:", store.conditions);
|
||||
|
||||
// Set mode from store or default
|
||||
// if (store.currentMode) {
|
||||
// setMode(store.currentMode);
|
||||
// } else {
|
||||
// setMode(mode.value);
|
||||
// }
|
||||
|
||||
// Initialize geometry from conditions
|
||||
// initializeGeometryFromConditions();
|
||||
} else {
|
||||
console.warn("Canvas ref is null on mounted");
|
||||
}
|
||||
});
|
||||
|
||||
// watch(
|
||||
// () => store.conditions,
|
||||
// () => {
|
||||
// initializeGeometryFromConditions();
|
||||
// }
|
||||
// );
|
||||
|
||||
// Watch store currentMode and update local mode and odontogram instance
|
||||
watch(
|
||||
() => store.currentMode,
|
||||
(newMode) => {
|
||||
if (newMode !== mode.value) {
|
||||
mode.value = newMode;
|
||||
odontogramInstance.value?.setMode(newMode);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Keep existing watch on local mode to update odontogram instance and store mode
|
||||
watch(mode, (newMode) => {
|
||||
console.log("Mode changed to:", newMode);
|
||||
odontogramInstance.value?.setMode(newMode);
|
||||
store.setMode(newMode);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.odontogram-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.odontogram-canvas {
|
||||
border: 1px solid #a9a9a9;
|
||||
margin-top: 15px;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.odontogram-canvas {
|
||||
max-width: auto;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<SharedWidgetCard title="Alat Kondisi Gigi" :icon="mdi - tooth">
|
||||
<v-row>
|
||||
<v-col cols="12" md="8">
|
||||
<div class="mode-grid">
|
||||
<v-btn
|
||||
v-for="mode in odontogramModes"
|
||||
:key="mode.value"
|
||||
:color="props.currentMode === mode.value ? 'primary' : 'default'"
|
||||
:variant="
|
||||
props.currentMode === mode.value ? 'elevated' : 'outlined'
|
||||
"
|
||||
size="small"
|
||||
flat
|
||||
class="mode-btn"
|
||||
@click="setMode(mode.value)"
|
||||
>
|
||||
<v-icon size="small" :color="mode.color">{{ mode.icon }}</v-icon>
|
||||
{{ mode.label }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="4">
|
||||
<v-btn color="error" variant="elevated" block @click="clearAll">
|
||||
<v-icon left>mdi-delete</v-icon>
|
||||
Hapus Semua
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</SharedWidgetCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SharedWidgetCard } from "#components";
|
||||
import { defineEmits, defineProps } from "vue";
|
||||
import { OdontogramMode } from "~/types/apps/medical/odontogram";
|
||||
|
||||
const props = defineProps({
|
||||
currentMode: {
|
||||
type: Number as () => OdontogramMode,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["modeChange", "clearAll"]);
|
||||
|
||||
const odontogramModes = [
|
||||
{
|
||||
value: OdontogramMode.DEFAULT,
|
||||
label: "Normal",
|
||||
icon: "mdi-cursor-default"
|
||||
},
|
||||
{
|
||||
value: OdontogramMode.AMF,
|
||||
label: "Amalgam",
|
||||
icon: "mdi-circle",
|
||||
color: "#222"
|
||||
},
|
||||
{
|
||||
value: OdontogramMode.COF,
|
||||
label: "Composite",
|
||||
icon: "mdi-circle",
|
||||
color: "#29b522"
|
||||
},
|
||||
{
|
||||
value: OdontogramMode.FIS,
|
||||
label: "Sealant",
|
||||
icon: "mdi-circle",
|
||||
color: "#ed3bed"
|
||||
},
|
||||
{ value: OdontogramMode.NVT, label: "Non-vital", icon: "mdi-triangle" },
|
||||
{ value: OdontogramMode.RCT, label: "Saluran Akar", icon: "mdi-triangle" },
|
||||
{ value: OdontogramMode.NON, label: "Tidak Ada", icon: "mdi-text" },
|
||||
{ value: OdontogramMode.UNE, label: "Un-Erupted", icon: "mdi-text" },
|
||||
{ value: OdontogramMode.PRE, label: "Partial-Erupt", icon: "mdi-text" },
|
||||
{ value: OdontogramMode.ANO, label: "Anomali", icon: "mdi-text" },
|
||||
{ value: OdontogramMode.CARIES, label: "Caries", icon: "mdi-border-color" },
|
||||
{ value: OdontogramMode.CFR, label: "Fraktur", icon: "mdi-pound" },
|
||||
{
|
||||
value: OdontogramMode.FMC,
|
||||
label: "Metal Crown",
|
||||
icon: "mdi-square-outline"
|
||||
},
|
||||
{ value: OdontogramMode.POC, label: "Porcelain Crown", icon: "mdi-square" },
|
||||
{ value: OdontogramMode.RRX, label: "Sisa Akar", icon: "mdi-tilde" },
|
||||
{ value: OdontogramMode.MIS, label: "Hilang", icon: "mdi-close" },
|
||||
{ value: OdontogramMode.IPX, label: "Implant", icon: "mdi-text" },
|
||||
{ value: OdontogramMode.FRM_ACR, label: "Denture", icon: "mdi-text" },
|
||||
{ value: OdontogramMode.BRIDGE, label: "Bridge", icon: "mdi-bridge" },
|
||||
{
|
||||
value: OdontogramMode.ARROW_TOP_LEFT,
|
||||
label: "Top Left Arrow",
|
||||
icon: "mdi-arrow-top-left"
|
||||
},
|
||||
{
|
||||
value: OdontogramMode.ARROW_TOP_RIGHT,
|
||||
label: "Top Right Arrow",
|
||||
icon: "mdi-arrow-top-right"
|
||||
},
|
||||
{
|
||||
value: OdontogramMode.ARROW_BOTTOM_LEFT,
|
||||
label: "Bottom Left Arrow",
|
||||
icon: "mdi-arrow-bottom-left"
|
||||
},
|
||||
{
|
||||
value: OdontogramMode.ARROW_BOTTOM_RIGHT,
|
||||
label: "Bottom Right Arrow",
|
||||
icon: "mdi-arrow-bottom-right"
|
||||
},
|
||||
{
|
||||
value: OdontogramMode.ARROW_TOP_TURN_LEFT,
|
||||
label: "Top Turn Left Arrow",
|
||||
icon: "mdi-arrow-top-left-bold"
|
||||
},
|
||||
{
|
||||
value: OdontogramMode.ARROW_TOP_TURN_RIGHT,
|
||||
label: "Top Turn Right Arrow",
|
||||
icon: "mdi-arrow-top-right-bold"
|
||||
},
|
||||
{
|
||||
value: OdontogramMode.ARROW_BOTTOM_TURN_LEFT,
|
||||
label: "Bottom Turn Left Arrow",
|
||||
icon: "mdi-arrow-bottom-left-bold"
|
||||
},
|
||||
{
|
||||
value: OdontogramMode.ARROW_BOTTOM_TURN_RIGHT,
|
||||
label: "Bottom Turn Right Arrow",
|
||||
icon: "mdi-arrow-bottom-right-bold"
|
||||
},
|
||||
{ value: OdontogramMode.HAPUS, label: "Hapus", icon: "mdi-eraser" }
|
||||
];
|
||||
|
||||
const setMode = (mode: OdontogramMode) => {
|
||||
emit("modeChange", mode);
|
||||
};
|
||||
|
||||
const clearAll = () => {
|
||||
if (confirm("Apakah Anda yakin ingin menghapus semua kondisi gigi?")) {
|
||||
emit("clearAll", true);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mode-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mode-btn {
|
||||
text-transform: none;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.mode-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
}
|
||||
|
||||
.mode-btn {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div class="tooth-element">
|
||||
<!-- Placeholder for individual tooth element -->
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// This component can be extended to handle individual tooth rendering or interaction
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tooth-element {
|
||||
/* Basic styling */
|
||||
display: inline-block;
|
||||
padding: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,223 @@
|
||||
<template>
|
||||
<v-card class="data-manager">
|
||||
<v-card-title>
|
||||
<v-icon left>mdi-database</v-icon>
|
||||
Manajemen Data
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="store.metadata.patientId"
|
||||
label="ID Pasien"
|
||||
prepend-icon="mdi-account"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
@input="store.saveCurrentData"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="store.metadata.dentist"
|
||||
label="Dokter Gigi"
|
||||
prepend-icon="mdi-doctor"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
@input="store.saveCurrentData"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="4">
|
||||
<v-btn color="success" variant="elevated" block @click="exportData">
|
||||
<v-icon left>mdi-download</v-icon>
|
||||
Simpan Data
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="4">
|
||||
<v-file-input
|
||||
ref="fileInput"
|
||||
accept=".json"
|
||||
style="display: none"
|
||||
@change="handleFileImport"
|
||||
/>
|
||||
<v-btn color="info" variant="elevated" block @click="importData">
|
||||
<v-icon left>mdi-upload</v-icon>
|
||||
Load Data
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="4">
|
||||
<v-btn color="error" variant="elevated" block @click="clearAllData">
|
||||
<v-icon left>mdi-delete-forever</v-icon>
|
||||
Hapus Semua
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider class="my-4" />
|
||||
|
||||
<div class="conditions-summary">
|
||||
<h4>Ringkasan Kondisi Gigi ({{ store.conditions.length }})</h4>
|
||||
<div class="d-flex flex-wrap align-center gap-3">
|
||||
<v-chip
|
||||
v-for="condition in store.conditions"
|
||||
:key="`${condition.toothNumber}-${condition.surface}`"
|
||||
size="small"
|
||||
closable
|
||||
:link="false"
|
||||
@click:close="removeCondition(condition)"
|
||||
:color="getSurfaceColor(condition.surface)"
|
||||
>
|
||||
{{ condition.toothNumber
|
||||
}}{{ condition.surface ? "-" + condition.surface : " " }}
|
||||
<span v-if="condition.mode === 18">
|
||||
{{
|
||||
condition.position &&
|
||||
condition.position.toLowerCase().includes("start")
|
||||
? "Start(Bridge)"
|
||||
: ""
|
||||
}}
|
||||
{{
|
||||
condition.position &&
|
||||
condition.position.toLowerCase().includes("finish")
|
||||
? "Finish(Bridge)"
|
||||
: ""
|
||||
}}
|
||||
</span>
|
||||
<span v-else> ({{ getModeLabel(condition.mode) }}) </span>
|
||||
<!-- <CircleXIcon class="ml-2" start size="20" /> -->
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
MoodSmileIcon,
|
||||
ChecksIcon,
|
||||
UserCircleIcon,
|
||||
CircleXIcon
|
||||
} from "vue-tabler-icons";
|
||||
import type { ToothCondition } from "~/types/apps/medical/odontogram";
|
||||
import { OdontogramMode } from "~/types/apps/medical/odontogram";
|
||||
import { useOdontogramStore } from "~/store/apps/medical/odontogram";
|
||||
import { useDataStorage } from "~/composables/apps/medical/useDataStorage";
|
||||
|
||||
const store = useOdontogramStore();
|
||||
const { exportData: exportDataUtil, importData: importDataUtil } =
|
||||
useDataStorage();
|
||||
|
||||
const fileInput = ref();
|
||||
|
||||
const modeLabels = {
|
||||
[OdontogramMode.DEFAULT]: "Normal",
|
||||
[OdontogramMode.AMF]: "Amalgam",
|
||||
[OdontogramMode.COF]: "Composite",
|
||||
[OdontogramMode.FIS]: "Sealant",
|
||||
[OdontogramMode.NVT]: "Non-vital",
|
||||
[OdontogramMode.RCT]: "Saluran Akar",
|
||||
[OdontogramMode.NON]: "Tidak Ada",
|
||||
[OdontogramMode.UNE]: "Un-Erupted",
|
||||
[OdontogramMode.PRE]: "Partial-Erupt",
|
||||
[OdontogramMode.ANO]: "Anomali",
|
||||
[OdontogramMode.CARIES]: "Caries",
|
||||
[OdontogramMode.CFR]: "Fraktur",
|
||||
[OdontogramMode.FMC]: "Metal Crown",
|
||||
[OdontogramMode.POC]: "Porcelain Crown",
|
||||
[OdontogramMode.RRX]: "Sisa Akar",
|
||||
[OdontogramMode.MIS]: "Hilang",
|
||||
[OdontogramMode.IPX]: "Implant",
|
||||
[OdontogramMode.FRM_ACR]: "Denture",
|
||||
[OdontogramMode.BRIDGE]: "Bridge",
|
||||
[OdontogramMode.ARROW_TOP_LEFT]: "Top Left Arrow",
|
||||
[OdontogramMode.ARROW_TOP_RIGHT]: "Top Right Arrow",
|
||||
[OdontogramMode.ARROW_BOTTOM_LEFT]: "Bottom Left Arrow",
|
||||
[OdontogramMode.ARROW_BOTTOM_RIGHT]: "Bottom Right Arrow",
|
||||
[OdontogramMode.ARROW_TOP_TURN_LEFT]: "Top Turn Left Arrow",
|
||||
[OdontogramMode.ARROW_TOP_TURN_RIGHT]: "Top Turn Right Arrow",
|
||||
[OdontogramMode.ARROW_BOTTOM_TURN_LEFT]: "Bottom Turn Left Arrow",
|
||||
[OdontogramMode.ARROW_BOTTOM_TURN_RIGHT]: "Bottom Turn Right Arrow",
|
||||
[OdontogramMode.HAPUS]: "Hapus"
|
||||
};
|
||||
|
||||
const getModeLabel = (mode: OdontogramMode) => {
|
||||
return modeLabels[mode] || "Unknown";
|
||||
};
|
||||
|
||||
const exportData = () => {
|
||||
const data = store.exportCurrentData();
|
||||
exportDataUtil(data);
|
||||
};
|
||||
|
||||
const importData = () => {
|
||||
fileInput.value?.$el.querySelector("input").click();
|
||||
};
|
||||
|
||||
const handleFileImport = async (event: any) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const data = await importDataUtil(file);
|
||||
if (data) {
|
||||
store.importData(data);
|
||||
// Clear file input
|
||||
event.target.value = "";
|
||||
} else {
|
||||
alert("Error importing data. Please check the file format.");
|
||||
}
|
||||
};
|
||||
|
||||
const clearAllData = () => {
|
||||
if (
|
||||
confirm("Apakah Anda yakin ingin menghapus semua data termasuk metadata?")
|
||||
) {
|
||||
store.clearAllConditions();
|
||||
store.metadata.patientId = "";
|
||||
store.metadata.dentist = "";
|
||||
store.metadata.date = new Date().toISOString().split("T")[0];
|
||||
}
|
||||
};
|
||||
|
||||
const removeCondition = (condition: ToothCondition) => {
|
||||
store.removeCondition(condition.toothNumber, condition.surface);
|
||||
};
|
||||
|
||||
const getSurfaceColor = (surface: string | undefined) => {
|
||||
switch (surface) {
|
||||
case "T":
|
||||
return "primary"; // Vuetify blue
|
||||
case "R":
|
||||
return "success"; // Vuetify green
|
||||
case "B":
|
||||
return "error"; // Vuetify red
|
||||
case "L":
|
||||
return "info"; // Vuetify yellow
|
||||
case "M":
|
||||
return "secondary"; // Custom purple, will add style below
|
||||
default:
|
||||
return "secondary";
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.data-manager {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.conditions-summary {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.conditions-summary h4 {
|
||||
margin-bottom: 12px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,236 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Alamat Identitas -->
|
||||
<v-card-subtitle
|
||||
class="bg-grey-lighten-4 text-subtitle-2 font-weight-medium py-2"
|
||||
>
|
||||
ALAMAT IDENTITAS
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<!-- Desa/Kelurahan -->
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="localAlamatIdentitas.desa"
|
||||
label="Desa/Kelurahan"
|
||||
placeholder="Cari desa atau kelurahan..."
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Alamat Lengkap -->
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="localAlamatIdentitas.alamatLengkap"
|
||||
label="Alamat Lengkap"
|
||||
placeholder="Alamat"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
rows="3"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- RT -->
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model="localAlamatIdentitas.rt"
|
||||
label="RT"
|
||||
placeholder="RT"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
counter="3"
|
||||
maxlength="3"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- RW -->
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model="localAlamatIdentitas.rw"
|
||||
label="RW"
|
||||
placeholder="RW"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
counter="3"
|
||||
maxlength="3"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Kode Pos -->
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model="localAlamatIdentitas.kodePos"
|
||||
label="Kode Pos"
|
||||
placeholder="Kode Pos"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
counter="5"
|
||||
maxlength="5"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Alamat Domisili -->
|
||||
<v-card-subtitle
|
||||
class="bg-grey-lighten-4 text-subtitle-2 font-weight-medium py-2"
|
||||
>
|
||||
ALAMAT DOMISILI
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text>
|
||||
<!-- Checkbox Samakan dengan alamat identitas -->
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-checkbox
|
||||
v-model="samaDenganIdentitas"
|
||||
label="Samakan dengan alamat identitas"
|
||||
density="compact"
|
||||
@change="handleSamaDenganIdentitas"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="!samaDenganIdentitas">
|
||||
<!-- Desa/Kelurahan -->
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="localAlamatDomisili.desa"
|
||||
label="Desa/Kelurahan"
|
||||
placeholder="Cari desa atau kelurahan..."
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Alamat Lengkap -->
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="localAlamatDomisili.alamatLengkap"
|
||||
label="Alamat Lengkap"
|
||||
placeholder="Alamat"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
rows="3"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- RT -->
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model="localAlamatDomisili.rt"
|
||||
label="RT"
|
||||
placeholder="RT"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
counter="3"
|
||||
maxlength="3"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- RW -->
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model="localAlamatDomisili.rw"
|
||||
label="RW"
|
||||
placeholder="RW"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
counter="3"
|
||||
maxlength="3"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Kode Pos -->
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model="localAlamatDomisili.kodePos"
|
||||
label="Kode Pos"
|
||||
placeholder="Kode Pos"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
counter="5"
|
||||
maxlength="5"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
alamatIdentitas: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
alamatDomisili: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
samaDenganIdentitas: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
"update:alamatIdentitas",
|
||||
"update:alamatDomisili",
|
||||
"update:samaDenganIdentitas"
|
||||
]);
|
||||
|
||||
const localAlamatIdentitas = ref({ ...props.alamatIdentitas });
|
||||
const localAlamatDomisili = ref({ ...props.alamatDomisili });
|
||||
const samaDenganIdentitas = ref(props.samaDenganIdentitas);
|
||||
|
||||
const rules = {
|
||||
required: (v) => !!v || "Field ini wajib diisi"
|
||||
};
|
||||
|
||||
const handleSamaDenganIdentitas = () => {
|
||||
if (samaDenganIdentitas.value) {
|
||||
localAlamatDomisili.value = { ...localAlamatIdentitas.value };
|
||||
} else {
|
||||
localAlamatDomisili.value = {
|
||||
desa: "",
|
||||
alamatLengkap: "",
|
||||
rt: "",
|
||||
rw: "",
|
||||
kodePos: ""
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for changes
|
||||
watch(
|
||||
localAlamatIdentitas,
|
||||
(newVal) => {
|
||||
emit("update:alamatIdentitas", newVal);
|
||||
if (samaDenganIdentitas.value) {
|
||||
localAlamatDomisili.value = { ...newVal };
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
localAlamatDomisili,
|
||||
(newVal) => {
|
||||
emit("update:alamatDomisili", newVal);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(samaDenganIdentitas, (newVal) => {
|
||||
emit("update:samaDenganIdentitas", newVal);
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,516 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Section Header -->
|
||||
<v-card-subtitle
|
||||
class="bg-grey-lighten-4 text-subtitle-2 font-weight-medium py-2"
|
||||
>
|
||||
DATA DIRI
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text>
|
||||
<!-- Checkbox Bayi Baru Lahir -->
|
||||
<v-row>
|
||||
<v-col cols="12" class="text-right py-0">
|
||||
<v-checkbox
|
||||
v-model="isBayiBaru"
|
||||
label="Bayi baru lahir"
|
||||
density="compact"
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<!-- Photo Upload -->
|
||||
<v-col cols="12" md="2" class="text-center">
|
||||
<v-avatar size="100" color="grey-lighten-3">
|
||||
<v-icon size="50" color="grey">mdi-account</v-icon>
|
||||
</v-avatar>
|
||||
<v-btn
|
||||
variant="text"
|
||||
color="primary"
|
||||
size="small"
|
||||
class="mt-2"
|
||||
@click="uploadPhoto"
|
||||
>
|
||||
Upload Foto
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<!-- Form Fields -->
|
||||
<v-col cols="12" md="10">
|
||||
<v-row>
|
||||
<!-- Nama Lengkap -->
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="localData.namaLengkap"
|
||||
label="Nama Lengkap"
|
||||
placeholder="Nama Lengkap"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
@input="debouncedParseNameToFhir"
|
||||
@paste="handlePaste"
|
||||
/>
|
||||
|
||||
<!-- FHIR Name Preview - Auto shows when typing -->
|
||||
<v-expand-transition>
|
||||
<v-alert
|
||||
v-if="fhirName && localData.namaLengkap && isTypingComplete"
|
||||
density="compact"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mt-2"
|
||||
closable
|
||||
@click:close="hideFhirPreview"
|
||||
>
|
||||
<template v-slot:title>
|
||||
<span class="text-body-2">FHIR Name Structure</span>
|
||||
</template>
|
||||
|
||||
<div class="text-caption mt-2">
|
||||
<v-row dense>
|
||||
<v-col
|
||||
v-if="fhirName.prefix && fhirName.prefix.length > 0"
|
||||
cols="12"
|
||||
>
|
||||
<v-chip
|
||||
size="x-small"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
class="mr-1"
|
||||
>
|
||||
Prefix
|
||||
</v-chip>
|
||||
<span class="font-weight-medium">{{
|
||||
fhirName.prefix.join(", ")
|
||||
}}</span>
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
v-if="fhirName.given && fhirName.given.length > 0"
|
||||
cols="12"
|
||||
>
|
||||
<v-chip
|
||||
size="x-small"
|
||||
color="success"
|
||||
variant="outlined"
|
||||
class="mr-1"
|
||||
>
|
||||
Given
|
||||
</v-chip>
|
||||
<span class="font-weight-medium">{{
|
||||
fhirName.given.join(" ")
|
||||
}}</span>
|
||||
</v-col>
|
||||
|
||||
<v-col v-if="fhirName.family" cols="12">
|
||||
<v-chip
|
||||
size="x-small"
|
||||
color="warning"
|
||||
variant="outlined"
|
||||
class="mr-1"
|
||||
>
|
||||
Family
|
||||
</v-chip>
|
||||
<span class="font-weight-medium">{{
|
||||
fhirName.family
|
||||
}}</span>
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
v-if="fhirName.suffix && fhirName.suffix.length > 0"
|
||||
cols="12"
|
||||
>
|
||||
<v-chip
|
||||
size="x-small"
|
||||
color="info"
|
||||
variant="outlined"
|
||||
class="mr-1"
|
||||
>
|
||||
Suffix
|
||||
</v-chip>
|
||||
<span class="font-weight-medium">{{
|
||||
fhirName.suffix.join(", ")
|
||||
}}</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-alert>
|
||||
</v-expand-transition>
|
||||
|
||||
<!-- Optional: Inline chips preview -->
|
||||
<div
|
||||
v-if="
|
||||
fhirName &&
|
||||
localData.namaLengkap &&
|
||||
isTypingComplete &&
|
||||
!showDetailedPreview
|
||||
"
|
||||
class="mt-2"
|
||||
>
|
||||
<v-chip-group>
|
||||
<v-chip
|
||||
v-for="(prefix, index) in fhirName.prefix || []"
|
||||
:key="`prefix-${index}`"
|
||||
size="x-small"
|
||||
color="primary"
|
||||
>
|
||||
{{ prefix }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-for="(given, index) in fhirName.given || []"
|
||||
:key="`given-${index}`"
|
||||
size="x-small"
|
||||
color="success"
|
||||
>
|
||||
{{ given }}
|
||||
</v-chip>
|
||||
<v-chip v-if="fhirName.family" size="x-small" color="warning">
|
||||
{{ fhirName.family }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-for="(suffix, index) in fhirName.suffix || []"
|
||||
:key="`suffix-${index}`"
|
||||
size="x-small"
|
||||
color="info"
|
||||
>
|
||||
{{ suffix }}
|
||||
</v-chip>
|
||||
</v-chip-group>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<!-- Nomor Telepon Selular -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.nomorTeleponSelular"
|
||||
label="Nomor Telepon Selular"
|
||||
placeholder="Nomor Telepon Selular"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.required, rules.phone]"
|
||||
required
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
v-bind="props"
|
||||
class="mr-2"
|
||||
>
|
||||
+62
|
||||
<v-icon end>mdi-menu-down</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item value="+62">+62</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
|
||||
<!-- Nomor Telepon Rumah -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.nomorTeleponRumah"
|
||||
label="Nomor Telepon Rumah"
|
||||
placeholder="Nomor Telepon Rumah"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
v-bind="props"
|
||||
class="mr-2"
|
||||
>
|
||||
+62
|
||||
<v-icon end>mdi-menu-down</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item value="+62">+62</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
|
||||
<!-- Email -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.email"
|
||||
label="Email"
|
||||
placeholder="Email"
|
||||
type="email"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.email]"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Jenis Kelamin -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.jenisKelamin"
|
||||
label="Jenis Kelamin"
|
||||
:items="jenisKelaminOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Tempat Lahir -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.tempatLahir"
|
||||
label="Tempat Lahir"
|
||||
placeholder="Tempat Lahir"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Tanggal Lahir -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.tanggalLahir"
|
||||
label="Tanggal Lahir"
|
||||
placeholder="Tanggal Lahir (DD/MM/YYYY)"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
>
|
||||
<template v-slot:append-inner>
|
||||
<v-icon>mdi-calendar</v-icon>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
|
||||
<!-- Jenis Identitas -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.jenisIdentitas"
|
||||
label="Jenis Identitas"
|
||||
:items="jenisIdentitasOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Nomor Identitas -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.nomorIdentitas"
|
||||
label="Nomor Identitas"
|
||||
placeholder="Nomor Identitas"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Nama Ibu Kandung -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.namaIbuKandung"
|
||||
label="Nama Ibu Kandung"
|
||||
placeholder="Nama Lengkap Ibu Kandung"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- No. Rekam Medis -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.nomorRekamMedis"
|
||||
label="No. Rekam Medis Lama / Manual"
|
||||
placeholder="No. Rekam medis"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from "vue";
|
||||
import { debounce } from "lodash-es"; // or use VueUse's useDebounceFn
|
||||
import {
|
||||
parseFhirHumanName,
|
||||
validateFhirHumanName
|
||||
} from "~/utils/module/fhirNameParser";
|
||||
import type { FhirHumanName } from "~/types/fhir/humanName";
|
||||
|
||||
// Types
|
||||
interface DataDiriForm {
|
||||
photo: File | null;
|
||||
namaLengkap: string;
|
||||
nomorTeleponSelular: string;
|
||||
nomorTeleponRumah: string;
|
||||
email: string;
|
||||
jenisKelamin: string;
|
||||
tempatLahir: string;
|
||||
tanggalLahir: string;
|
||||
jenisIdentitas: string;
|
||||
nomorIdentitas: string;
|
||||
namaIbuKandung: string;
|
||||
nomorRekamMedis: string;
|
||||
fhirName?: FhirHumanName | null;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
formData: DataDiriForm;
|
||||
isBayiBaru: boolean;
|
||||
}
|
||||
|
||||
// Props & Emits
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:formData": [value: DataDiriForm];
|
||||
"update:isBayiBaru": [value: boolean];
|
||||
}>();
|
||||
|
||||
// Reactive data
|
||||
const localData = ref<DataDiriForm>({ ...props.formData });
|
||||
const isBayiBaru = ref(props.isBayiBaru);
|
||||
const fhirName = ref<FhirHumanName | null>(null);
|
||||
const isTypingComplete = ref(false);
|
||||
const showDetailedPreview = ref(true); // Toggle between detailed and chip view
|
||||
|
||||
// Rules
|
||||
const rules = {
|
||||
required: (v: string) => !!v || "Field ini wajib diisi",
|
||||
email: (v: string) => !v || /.+@.+\..+/.test(v) || "Format email tidak valid",
|
||||
phone: (v: string) =>
|
||||
!v || /^[0-9+\-() ]+$/.test(v) || "Format nomor telepon tidak valid"
|
||||
};
|
||||
|
||||
// Options
|
||||
interface SelectOption {
|
||||
title: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const jenisKelaminOptions: SelectOption[] = [
|
||||
{ title: "Laki-laki", value: "L" },
|
||||
{ title: "Perempuan", value: "P" }
|
||||
];
|
||||
|
||||
const jenisIdentitasOptions: SelectOption[] = [
|
||||
{ title: "KTP", value: "KTP" },
|
||||
{ title: "SIM", value: "SIM" },
|
||||
{ title: "Paspor", value: "PASSPORT" }
|
||||
];
|
||||
|
||||
// Methods
|
||||
import { formatFhirName } from "~/utils/module/fhirNameParser";
|
||||
|
||||
const parseNameToFhir = (): void => {
|
||||
if (localData.value.namaLengkap && localData.value.namaLengkap.trim()) {
|
||||
fhirName.value = parseFhirHumanName(localData.value.namaLengkap);
|
||||
|
||||
if (fhirName.value) {
|
||||
const errors = validateFhirHumanName(fhirName.value);
|
||||
if (errors.length > 0) {
|
||||
console.warn("FHIR Name validation errors:", errors);
|
||||
}
|
||||
|
||||
// Store FHIR name in form data
|
||||
localData.value.fhirName = fhirName.value;
|
||||
|
||||
// Format the FHIR name back to a string and update namaLengkap
|
||||
const formattedName = formatFhirName(fhirName.value);
|
||||
if (formattedName !== localData.value.namaLengkap) {
|
||||
localData.value.namaLengkap = formattedName;
|
||||
}
|
||||
|
||||
// Show preview after parsing
|
||||
isTypingComplete.value = true;
|
||||
}
|
||||
} else {
|
||||
// Clear FHIR name if input is empty
|
||||
fhirName.value = null;
|
||||
localData.value.fhirName = null;
|
||||
isTypingComplete.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Debounced version of parseNameToFhir (wait 500ms after user stops typing)
|
||||
const debouncedParseNameToFhir = debounce(() => {
|
||||
parseNameToFhir();
|
||||
}, 500);
|
||||
|
||||
// Handle paste event
|
||||
const handlePaste = (event: ClipboardEvent): void => {
|
||||
// Reset typing complete flag
|
||||
isTypingComplete.value = false;
|
||||
|
||||
// Parse after a short delay to ensure v-model is updated
|
||||
setTimeout(() => {
|
||||
parseNameToFhir();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// Hide FHIR preview
|
||||
const hideFhirPreview = (): void => {
|
||||
isTypingComplete.value = false;
|
||||
};
|
||||
|
||||
const uploadPhoto = (): void => {
|
||||
// Implementation for photo upload
|
||||
console.log("Upload photo clicked");
|
||||
};
|
||||
|
||||
// Watchers
|
||||
watch(
|
||||
localData,
|
||||
(newVal) => {
|
||||
emit("update:formData", newVal);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(isBayiBaru, (newVal) => {
|
||||
emit("update:isBayiBaru", newVal);
|
||||
});
|
||||
|
||||
// Reset typing complete when name is cleared
|
||||
watch(
|
||||
() => localData.value.namaLengkap,
|
||||
(newVal) => {
|
||||
if (!newVal) {
|
||||
isTypingComplete.value = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Optional: Add smooth transitions */
|
||||
.v-chip-group {
|
||||
gap: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card-subtitle
|
||||
class="bg-grey-lighten-4 text-subtitle-2 font-weight-medium py-2"
|
||||
>
|
||||
KESEHATAN
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<!-- Golongan Darah -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.golonganDarah"
|
||||
label="Golongan Darah"
|
||||
:items="golonganDarahOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Status Perokok -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.statusPerokok"
|
||||
label="Status Perokok"
|
||||
:items="statusPerokokOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Jenis Penyakit -->
|
||||
<v-col cols="12">
|
||||
<v-select
|
||||
v-model="localData.jenisPenyakit"
|
||||
label="Jenis Penyakit"
|
||||
:items="jenisPenyakitOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
multiple
|
||||
chips
|
||||
closable-chips
|
||||
placeholder="Pilih Jenis Penyakit"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
formData: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:formData"]);
|
||||
|
||||
const localData = ref({ ...props.formData });
|
||||
|
||||
const golonganDarahOptions = [
|
||||
{ title: "A", value: "A" },
|
||||
{ title: "B", value: "B" },
|
||||
{ title: "AB", value: "AB" },
|
||||
{ title: "O", value: "O" }
|
||||
];
|
||||
|
||||
const statusPerokokOptions = [
|
||||
{ title: "Tidak Merokok", value: "tidak" },
|
||||
{ title: "Perokok Aktif", value: "aktif" },
|
||||
{ title: "Perokok Pasif", value: "pasif" },
|
||||
{ title: "Mantan Perokok", value: "mantan" }
|
||||
];
|
||||
|
||||
const jenisPenyakitOptions = [
|
||||
"Diabetes",
|
||||
"Hipertensi",
|
||||
"Jantung",
|
||||
"Asma",
|
||||
"TBC",
|
||||
"Hepatitis",
|
||||
"HIV/AIDS",
|
||||
"Kanker",
|
||||
"Ginjal",
|
||||
"Stroke"
|
||||
];
|
||||
|
||||
watch(
|
||||
localData,
|
||||
(newVal) => {
|
||||
emit("update:formData", newVal);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card-subtitle class="bg-grey-lighten-4 text-subtitle-2 font-weight-medium py-2">
|
||||
PEMBAYARAN
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-select
|
||||
v-model="localData.jenisPembayaran"
|
||||
label="Pembayaran"
|
||||
:items="pembayaranOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
formData: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:formData'])
|
||||
|
||||
const localData = ref({ ...props.formData })
|
||||
|
||||
const rules = {
|
||||
required: v => !!v || 'Field ini wajib diisi'
|
||||
}
|
||||
|
||||
const pembayaranOptions = [
|
||||
{ title: 'BPJS', value: 'BPJS' },
|
||||
{ title: 'Umum', value: 'UMUM' },
|
||||
{ title: 'Asuransi', value: 'ASURANSI' },
|
||||
{ title: 'Perusahaan', value: 'PERUSAHAAN' }
|
||||
]
|
||||
|
||||
watch(localData, (newVal) => {
|
||||
emit('update:formData', newVal)
|
||||
}, { deep: true })
|
||||
</script>
|
||||
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card-subtitle
|
||||
class="bg-grey-lighten-4 text-subtitle-2 font-weight-medium py-2"
|
||||
>
|
||||
PENANGGUNG JAWAB / WALI PASIEN
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<!-- Nama Lengkap -->
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="localData.namaLengkap"
|
||||
label="Nama Lengkap"
|
||||
placeholder="Nama Lengkap"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Jenis Kelamin -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.jenisKelamin"
|
||||
label="Jenis Kelamin"
|
||||
:items="jenisKelaminOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Hubungan dengan Pasien -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.hubunganDenganPasien"
|
||||
label="Hubungan dengan Pasien"
|
||||
:items="hubunganOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Nomor Telepon -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.nomorTelepon"
|
||||
label="Nomor Telepon Selular / Rumah"
|
||||
placeholder="Nomor Telepon"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
v-bind="props"
|
||||
class="mr-2"
|
||||
>
|
||||
+62
|
||||
<v-icon end>mdi-menu-down</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item value="+62">+62</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
|
||||
<!-- Pekerjaan -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.pekerjaan"
|
||||
label="Pekerjaan"
|
||||
placeholder="Pekerjaan"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Alamat -->
|
||||
<v-col cols="12">
|
||||
<label class="text-subtitle-2 font-weight-medium">Alamat</label>
|
||||
<v-checkbox
|
||||
v-model="samaDenganPasien"
|
||||
label="Samakan dengan alamat pasien"
|
||||
density="compact"
|
||||
@change="handleSamakanAlamat"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" v-if="!samaDenganPasien">
|
||||
<v-textarea
|
||||
v-model="localData.alamat"
|
||||
placeholder="Alamat"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
rows="3"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
formData: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
samaDenganPasien: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
alamatPasien: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:formData", "update:samaDenganPasien"]);
|
||||
|
||||
const localData = ref({ ...props.formData });
|
||||
const samaDenganPasien = ref(props.samaDenganPasien);
|
||||
|
||||
const jenisKelaminOptions = [
|
||||
{ title: "Laki-laki", value: "L" },
|
||||
{ title: "Perempuan", value: "P" }
|
||||
];
|
||||
|
||||
const hubunganOptions = [
|
||||
"Suami",
|
||||
"Istri",
|
||||
"Anak",
|
||||
"Orang Tua",
|
||||
"Saudara",
|
||||
"Kerabat",
|
||||
"Lainnya"
|
||||
];
|
||||
|
||||
const handleSamakanAlamat = () => {
|
||||
if (samaDenganPasien.value && props.alamatPasien) {
|
||||
localData.value.alamat = props.alamatPasien.alamatLengkap || "";
|
||||
} else {
|
||||
localData.value.alamat = "";
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
localData,
|
||||
(newVal) => {
|
||||
emit("update:formData", newVal);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(samaDenganPasien, (newVal) => {
|
||||
emit("update:samaDenganPasien", newVal);
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card-subtitle
|
||||
class="bg-grey-lighten-4 text-subtitle-2 font-weight-medium py-2"
|
||||
>
|
||||
SOSIAL
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<!-- Agama -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.agama"
|
||||
label="Agama"
|
||||
:items="agamaOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Status Pernikahan -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.statusPernikahan"
|
||||
label="Status Pernikahan"
|
||||
:items="statusPernikahanOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Pendidikan Terakhir -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.pendidikanTerakhir"
|
||||
label="Pendidikan Terakhir"
|
||||
:items="pendidikanOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Pekerjaan -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.pekerjaan"
|
||||
label="Pekerjaan"
|
||||
:items="pekerjaanOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Bahasa yang Dikuasai -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.bahasaDikuasai"
|
||||
label="Bahasa yang Dikuasai"
|
||||
placeholder="Bahasa yang Dikuasai"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Suku/Etnis -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.sukuEtnis"
|
||||
label="Suku/Etnis"
|
||||
placeholder="Suku/Etnis"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
formData: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:formData"]);
|
||||
|
||||
const localData = ref({ ...props.formData });
|
||||
|
||||
const agamaOptions = [
|
||||
"Islam",
|
||||
"Kristen",
|
||||
"Katolik",
|
||||
"Hindu",
|
||||
"Buddha",
|
||||
"Konghucu",
|
||||
"Lainnya"
|
||||
];
|
||||
|
||||
const statusPernikahanOptions = [
|
||||
{ title: "Belum Menikah", value: "belum_menikah" },
|
||||
{ title: "Menikah", value: "menikah" },
|
||||
{ title: "Cerai Hidup", value: "cerai_hidup" },
|
||||
{ title: "Cerai Mati", value: "cerai_mati" }
|
||||
];
|
||||
|
||||
const pendidikanOptions = [
|
||||
"Tidak Sekolah",
|
||||
"SD",
|
||||
"SMP",
|
||||
"SMA/SMK",
|
||||
"D1",
|
||||
"D3",
|
||||
"D4/S1",
|
||||
"S2",
|
||||
"S3"
|
||||
];
|
||||
|
||||
const pekerjaanOptions = [
|
||||
"PNS",
|
||||
"TNI/Polri",
|
||||
"Pegawai Swasta",
|
||||
"Wiraswasta",
|
||||
"Petani",
|
||||
"Nelayan",
|
||||
"Buruh",
|
||||
"Ibu Rumah Tangga",
|
||||
"Pelajar/Mahasiswa",
|
||||
"Tidak Bekerja",
|
||||
"Lainnya"
|
||||
];
|
||||
|
||||
watch(
|
||||
localData,
|
||||
(newVal) => {
|
||||
emit("update:formData", newVal);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,227 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<span class="text-h6">NAKES & JADWAL</span>
|
||||
<v-btn variant="text" color="primary" size="small" @click="resetSection">
|
||||
Atur Ulang
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.paymentMethod"
|
||||
:items="paymentMethods"
|
||||
label="Pembiayaan"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.required]"
|
||||
append-inner-icon="mdi-chevron-down"
|
||||
>
|
||||
<template v-slot:label>
|
||||
Pembiayaan <span class="text-red">*</span>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.referralNumber"
|
||||
label="Nomor Rujukan (Opsional)"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="Nomor Rujukan"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-select
|
||||
v-model="localData.polyclinicName"
|
||||
:items="polyclinics"
|
||||
label="Nama Poliklinik"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="Pilih"
|
||||
:rules="[rules.required]"
|
||||
@update:modelValue="onPolyclinicChange"
|
||||
append-inner-icon="mdi-chevron-down"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-select
|
||||
v-model="localData.doctorName"
|
||||
:items="doctors"
|
||||
label="Nama Nakes"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="Pilih"
|
||||
:rules="[rules.required]"
|
||||
:disabled="!localData.polyclinicName"
|
||||
@update:modelValue="onDoctorChange"
|
||||
append-inner-icon="mdi-chevron-down"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.consultationDate"
|
||||
label="Tanggal Konsultasi"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
type="date"
|
||||
:rules="[rules.required]"
|
||||
:min="minDate"
|
||||
>
|
||||
<template v-slot:label>
|
||||
Tanggal Konsultasi <span class="text-red">*</span>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.consultationTime"
|
||||
:items="timeSlots"
|
||||
label="Jam Konsultasi"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="Pilih Jam Konsultasi"
|
||||
:rules="[rules.required]"
|
||||
:disabled="!localData.consultationDate || !localData.doctorName"
|
||||
append-inner-icon="mdi-clock-outline"
|
||||
>
|
||||
<template v-slot:label>
|
||||
Jam Konsultasi <span class="text-red">*</span>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="localData.doctorName && localData.consultationDate">
|
||||
<v-col cols="12">
|
||||
<v-alert type="info" variant="tonal" density="compact">
|
||||
Slot Nakes: {{ availableSlots }} slot tersedia
|
||||
</v-alert>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
scheduleData: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:scheduleData", "update"]);
|
||||
|
||||
const localData = ref({ ...props.scheduleData });
|
||||
|
||||
const paymentMethods = ["Pribadi", "BPJS", "Asuransi Swasta", "Perusahaan"];
|
||||
|
||||
const polyclinics = [
|
||||
"Poli Umum",
|
||||
"Poli Gigi",
|
||||
"Poli Anak",
|
||||
"Poli Kandungan",
|
||||
"Poli Penyakit Dalam",
|
||||
"Poli THT"
|
||||
];
|
||||
|
||||
const doctors = ref([]);
|
||||
const timeSlots = ref([]);
|
||||
const availableSlots = ref(0);
|
||||
|
||||
const minDate = computed(() => {
|
||||
const today = new Date();
|
||||
return today.toISOString().split("T")[0];
|
||||
});
|
||||
|
||||
const rules = {
|
||||
required: (value) => !!value || "Field ini wajib diisi"
|
||||
};
|
||||
|
||||
const onPolyclinicChange = (value) => {
|
||||
// Reset doctor and time when polyclinic changes
|
||||
localData.value.doctorName = "";
|
||||
localData.value.consultationTime = "";
|
||||
|
||||
// Simulate loading doctors based on polyclinic
|
||||
if (value) {
|
||||
doctors.value = [
|
||||
"dr. Ahmad Subhan, Sp.A",
|
||||
"dr. Siti Nurhaliza",
|
||||
"dr. Budi Santoso, Sp.PD",
|
||||
"dr. Maria Christina"
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
const onDoctorChange = (value) => {
|
||||
// Reset time when doctor changes
|
||||
localData.value.consultationTime = "";
|
||||
|
||||
// Simulate loading time slots
|
||||
if (value && localData.value.consultationDate) {
|
||||
loadTimeSlots();
|
||||
}
|
||||
};
|
||||
|
||||
const loadTimeSlots = () => {
|
||||
// Simulate available time slots
|
||||
timeSlots.value = [
|
||||
{ title: "08:00 - 08:30", value: "08:00" },
|
||||
{ title: "08:30 - 09:00", value: "08:30" },
|
||||
{ title: "09:00 - 09:30", value: "09:00" },
|
||||
{ title: "09:30 - 10:00", value: "09:30" },
|
||||
{ title: "10:00 - 10:30", value: "10:00" },
|
||||
{ title: "10:30 - 11:00", value: "10:30" }
|
||||
];
|
||||
availableSlots.value = 6;
|
||||
};
|
||||
|
||||
const resetSection = () => {
|
||||
localData.value = {
|
||||
paymentMethod: "Pribadi",
|
||||
referralNumber: "",
|
||||
polyclinicName: "",
|
||||
doctorName: "",
|
||||
consultationDate: "",
|
||||
consultationTime: "",
|
||||
slotAvailable: true
|
||||
};
|
||||
doctors.value = [];
|
||||
timeSlots.value = [];
|
||||
};
|
||||
|
||||
watch(
|
||||
localData,
|
||||
(newVal) => {
|
||||
emit("update:scheduleData", newVal);
|
||||
emit("update");
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => localData.value.consultationDate,
|
||||
(newVal) => {
|
||||
if (newVal && localData.value.doctorName) {
|
||||
loadTimeSlots();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,249 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<span class="text-h6">DATA PASIEN</span>
|
||||
<v-btn variant="text" color="primary" size="small" @click="resetSection">
|
||||
Atur Ulang
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="localData.name"
|
||||
label="Nama Pasien"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="Masukkan Nama Pasien"
|
||||
:rules="[rules.required]"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.medicalRecordNumber"
|
||||
label="No. Rekam medis"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="MRN Pasien"
|
||||
:rules="[rules.required]"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.category"
|
||||
:items="patientCategories"
|
||||
label="Kategori Pasien"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="Pilih Kategori Pasien"
|
||||
append-inner-icon="mdi-chevron-down"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.birthDate"
|
||||
label="Tanggal Lahir"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
type="date"
|
||||
placeholder="Tanggal Lahir (DD/MM/YYYY)"
|
||||
:rules="[rules.required]"
|
||||
>
|
||||
<template v-slot:label>
|
||||
Tanggal Lahir <span class="text-red">*</span>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.gender"
|
||||
:items="genderOptions"
|
||||
label="Jenis Kelamin"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="Pilih"
|
||||
:rules="[rules.required]"
|
||||
append-inner-icon="mdi-chevron-down"
|
||||
>
|
||||
<template v-slot:label>
|
||||
Jenis Kelamin <span class="text-red">*</span>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="localData.address"
|
||||
label="Alamat"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
rows="3"
|
||||
placeholder="Alamat"
|
||||
:rules="[rules.required]"
|
||||
counter="100"
|
||||
>
|
||||
<template v-slot:label>
|
||||
Alamat <span class="text-red">*</span>
|
||||
</template>
|
||||
</v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="2">
|
||||
<v-select
|
||||
v-model="localData.phoneCode"
|
||||
:items="phoneCodes"
|
||||
label="Kode"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.required]"
|
||||
>
|
||||
<template v-slot:label>
|
||||
Kode <span class="text-red">*</span>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="5">
|
||||
<v-text-field
|
||||
v-model="localData.phoneNumber"
|
||||
label="Nomor Telepon"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="Nomor Telepon"
|
||||
:rules="[rules.required, rules.phone]"
|
||||
>
|
||||
<template v-slot:label>
|
||||
Nomor Telepon <span class="text-red">*</span>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="5">
|
||||
<v-text-field
|
||||
v-model="localData.email"
|
||||
label="Email"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="Email"
|
||||
:rules="[rules.email]"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.identityType"
|
||||
:items="identityTypes"
|
||||
label="Jenis Identitas"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="Pilih"
|
||||
:rules="[rules.required]"
|
||||
append-inner-icon="mdi-chevron-down"
|
||||
>
|
||||
<template v-slot:label>
|
||||
Jenis Identitas <span class="text-red">*</span>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.identityNumber"
|
||||
label="Nomor Identitas"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="Nomor Identitas"
|
||||
:rules="[rules.required]"
|
||||
>
|
||||
<template v-slot:label>
|
||||
Nomor Identitas <span class="text-red">*</span>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
patientData: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:patientData", "update"]);
|
||||
|
||||
const localData = ref({ ...props.patientData });
|
||||
|
||||
const patientCategories = ["Umum", "BPJS", "Asuransi Lain"];
|
||||
|
||||
const genderOptions = [
|
||||
{ title: "Laki-laki", value: "L" },
|
||||
{ title: "Perempuan", value: "P" }
|
||||
];
|
||||
|
||||
const phoneCodes = ["+62", "+65", "+60", "+1"];
|
||||
|
||||
const identityTypes = [
|
||||
{ title: "KTP", value: "ktp" },
|
||||
{ title: "SIM", value: "sim" },
|
||||
{ title: "Paspor", value: "paspor" },
|
||||
{ title: "Kartu Pelajar", value: "kartu_pelajar" }
|
||||
];
|
||||
|
||||
const rules = {
|
||||
required: (value) => !!value || "Field ini wajib diisi",
|
||||
email: (value) => {
|
||||
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return !value || pattern.test(value) || "Email tidak valid";
|
||||
},
|
||||
phone: (value) => {
|
||||
const pattern = /^[0-9]{8,15}$/;
|
||||
return pattern.test(value) || "Nomor telepon tidak valid";
|
||||
}
|
||||
};
|
||||
|
||||
const resetSection = () => {
|
||||
localData.value = {
|
||||
name: "",
|
||||
medicalRecordNumber: "",
|
||||
category: "",
|
||||
birthDate: "",
|
||||
gender: "",
|
||||
address: "",
|
||||
phoneCode: "+62",
|
||||
phoneNumber: "",
|
||||
email: "",
|
||||
identityType: "",
|
||||
identityNumber: ""
|
||||
};
|
||||
};
|
||||
|
||||
watch(
|
||||
localData,
|
||||
(newVal) => {
|
||||
emit("update:patientData", newVal);
|
||||
emit("update");
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<span class="text-h6">JENIS PENDAFTARAN</span>
|
||||
<v-btn variant="text" color="primary" size="small" @click="resetSection">
|
||||
Atur Ulang
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<div class="mb-2">
|
||||
<span class="text-subtitle-2">
|
||||
Jenis Kunjungan <span class="text-red">*</span>
|
||||
</span>
|
||||
</div>
|
||||
<v-radio-group
|
||||
v-model="localData.visitType"
|
||||
:rules="[rules.required]"
|
||||
inline
|
||||
>
|
||||
<v-radio label="Kunjungan Sakit" value="sakit" />
|
||||
<v-radio label="Kunjungan Sehat" value="sehat" />
|
||||
</v-radio-group>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.treatmentType"
|
||||
:items="treatmentTypes"
|
||||
label="Jenis Perawatan"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.required]"
|
||||
append-inner-icon="mdi-chevron-down"
|
||||
>
|
||||
<template v-slot:label>
|
||||
Jenis Perawatan <span class="text-red">*</span>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.triage"
|
||||
:items="triageOptions"
|
||||
label="Triase"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="Pilih"
|
||||
clearable
|
||||
append-inner-icon="mdi-chevron-down"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
registrationData: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:registrationData", "update"]);
|
||||
|
||||
const localData = ref({ ...props.registrationData });
|
||||
|
||||
const treatmentTypes = ["Rawat Jalan", "Rawat Inap", "IGD"];
|
||||
|
||||
const triageOptions = [
|
||||
{ title: "Merah (Emergency)", value: "merah" },
|
||||
{ title: "Kuning (Urgent)", value: "kuning" },
|
||||
{ title: "Hijau (Less Urgent)", value: "hijau" },
|
||||
{ title: "Hitam (Non Urgent)", value: "hitam" }
|
||||
];
|
||||
|
||||
const rules = {
|
||||
required: (value) => !!value || "Field ini wajib diisi"
|
||||
};
|
||||
|
||||
const resetSection = () => {
|
||||
localData.value = {
|
||||
visitType: "",
|
||||
treatmentType: "Rawat Jalan",
|
||||
triage: ""
|
||||
};
|
||||
};
|
||||
|
||||
watch(
|
||||
localData,
|
||||
(newVal) => {
|
||||
emit("update:registrationData", newVal);
|
||||
emit("update");
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<div class="d-flex align-center gap-2">
|
||||
<span class="text-h6">SKALA RISIKO JATUH (OPSIONAL)</span>
|
||||
<v-icon color="warning" size="small">mdi-alert-box</v-icon>
|
||||
</div>
|
||||
<v-btn variant="text" color="primary" size="small" @click="resetSection">
|
||||
Atur Ulang
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-expansion-panels v-model="panel" class="mt-2">
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-title>
|
||||
<span class="font-weight-medium"
|
||||
>SKALA RISIKO JATUH - GET UP AND GO</span
|
||||
>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<v-card flat>
|
||||
<v-card-text>
|
||||
<!-- Walking Ability Question -->
|
||||
<div class="mb-6">
|
||||
<div class="mb-3">
|
||||
<p class="text-subtitle-1 font-weight-medium mb-1">
|
||||
Cara berjalan pasien:
|
||||
</p>
|
||||
<ul class="ml-4">
|
||||
<li>Jalan tidak seimbang, sempoyongan, atau limbung</li>
|
||||
<li>
|
||||
Menggunakan alat bantu (kruk, tripot, kursi roda, orang
|
||||
lain)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<v-radio-group
|
||||
v-model="localData.walkingAbility"
|
||||
inline
|
||||
@update:modelValue="calculateScore"
|
||||
>
|
||||
<v-radio label="Ya" :value="true" />
|
||||
<v-radio label="Tidak" :value="false" />
|
||||
</v-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- Sitting Support Question -->
|
||||
<div class="mb-6">
|
||||
<div class="mb-3">
|
||||
<p class="text-subtitle-1 font-weight-medium mb-1">
|
||||
Menopang saat akan duduk
|
||||
</p>
|
||||
<p class="text-body-2 text-grey-darken-1">
|
||||
Tampak memegang sandaran kursi atau meja / benda lain
|
||||
sebagai penopang saat akan duduk
|
||||
</p>
|
||||
</div>
|
||||
<v-radio-group
|
||||
v-model="localData.supportWhileSitting"
|
||||
inline
|
||||
@update:modelValue="calculateScore"
|
||||
>
|
||||
<v-radio label="Ya" :value="true" />
|
||||
<v-radio label="Tidak" :value="false" />
|
||||
</v-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- Score Display -->
|
||||
<v-divider class="my-4" />
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<span class="text-subtitle-1 font-weight-medium">Skor</span>
|
||||
<v-chip :color="scoreColor" variant="tonal" size="large">
|
||||
{{ localData.score }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<!-- Risk Level Alert -->
|
||||
<v-alert
|
||||
v-if="riskLevel"
|
||||
:type="riskLevel.type"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
class="mt-4"
|
||||
>
|
||||
{{ riskLevel.message }}
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
riskData: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:riskData", "update"]);
|
||||
|
||||
const localData = ref({ ...props.riskData });
|
||||
const panel = ref(0);
|
||||
|
||||
const calculateScore = () => {
|
||||
let score = 0;
|
||||
if (localData.value.walkingAbility === true) score += 1;
|
||||
if (localData.value.supportWhileSitting === true) score += 1;
|
||||
localData.value.score = score;
|
||||
};
|
||||
|
||||
const scoreColor = computed(() => {
|
||||
if (localData.value.score === 0) return "success";
|
||||
if (localData.value.score === 1) return "warning";
|
||||
return "error";
|
||||
});
|
||||
|
||||
const riskLevel = computed(() => {
|
||||
if (localData.value.score === 0) {
|
||||
return {
|
||||
type: "success",
|
||||
message: "Risiko Rendah: Tidak berisiko jatuh"
|
||||
};
|
||||
} else if (localData.value.score === 1) {
|
||||
return {
|
||||
type: "warning",
|
||||
message: "Risiko Sedang: Memerlukan pengawasan"
|
||||
};
|
||||
} else if (localData.value.score === 2) {
|
||||
return {
|
||||
type: "error",
|
||||
message: "Risiko Tinggi: Memerlukan bantuan dan pengawasan ketat"
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const resetSection = () => {
|
||||
localData.value = {
|
||||
walkingAbility: null,
|
||||
supportWhileSitting: null,
|
||||
score: 0
|
||||
};
|
||||
};
|
||||
|
||||
watch(
|
||||
localData,
|
||||
(newVal) => {
|
||||
emit("update:riskData", newVal);
|
||||
emit("update");
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
Reference in New Issue
Block a user