Added Organ Selector
parent
8068de70cb
commit
757ace2ba1
Binary file not shown.
|
After Width: | Height: | Size: 220 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 224 KiB |
@ -0,0 +1,10 @@
|
||||
<svg width="34" height="34" viewBox="0 0 34 34" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1_27024)">
|
||||
<path d="M16.9795 19.2095C16.93 19.7925 17.3257 22.8783 17.5299 24.0883C17.6272 24.6615 17.8937 25.5955 17.7978 26.0914C17.6609 26.7825 17.6213 27.6769 17.6967 28.1976C17.7442 28.5139 17.8939 29.9725 17.6798 30.506C17.5677 30.7858 17.3624 32.2195 17.3624 32.2195C16.8274 33.5702 17.1293 33.5038 17.1293 33.5038C17.2949 33.707 17.5786 33.5197 17.5786 33.5197C17.7947 33.6575 17.9444 33.487 17.9444 33.487C18.1299 33.6405 18.3461 33.4681 18.3461 33.4681C18.5792 33.5891 18.7953 33.366 18.7953 33.366C18.9291 33.4334 18.9619 33.3482 18.9619 33.3482C19.3635 33.3225 18.7378 32.0373 18.7378 32.0373C18.588 30.883 18.8865 30.2406 18.8865 30.2406C19.8642 27.3411 19.9139 26.5716 19.5231 25.4789C19.413 25.1634 19.3851 25.0385 19.4358 24.9016C19.5529 24.5855 19.4675 23.3141 19.6103 22.8093C19.8859 21.8356 20.1577 19.3663 20.2994 18.2142C20.4897 16.6622 19.625 14.5813 19.625 14.5813C19.4356 13.7345 19.7133 10.7174 19.7133 10.7174C20.101 11.3208 20.0862 12.3858 20.0862 12.3858C20.0246 13.5028 20.9885 15.2099 20.9885 15.2099C21.4517 15.9154 21.6271 16.5847 21.6271 16.6344C21.6271 16.8376 21.5826 17.3296 21.5826 17.3296L21.6004 17.758C21.6083 17.8671 21.6697 18.2428 21.6598 18.4244C21.5875 19.5419 21.765 19.3317 21.765 19.3317C21.9148 19.3317 22.0795 18.4324 22.0795 18.4324C22.0795 18.6643 22.0228 19.3585 22.1479 19.6204C22.2975 19.9327 22.4076 19.5668 22.4095 19.4934C22.4491 18.0705 22.5346 18.4433 22.5346 18.4433C22.6178 19.5976 22.7201 19.8583 22.9035 19.7681C23.0424 19.7018 22.9154 18.3829 22.9154 18.3829C23.1533 19.1664 23.3337 19.2911 23.3337 19.2911C23.7263 19.5668 23.4835 18.8053 23.429 18.6545C23.1385 17.8533 23.1295 17.5757 23.1295 17.5757C23.4925 18.2956 23.7661 18.2689 23.7661 18.2689C24.1201 18.1559 23.4567 17.1364 23.0679 16.6481C22.8696 16.3992 22.6138 16.066 22.5395 15.8682C22.4185 15.533 22.3271 14.4556 22.3271 14.4556C22.2905 13.1843 21.9762 12.632 21.9762 12.632C21.4388 11.7719 21.3377 10.1673 21.3377 10.1673L21.3139 7.45832C21.1255 5.61048 19.7639 5.59712 19.7639 5.59712C18.3877 5.39226 18.1962 4.94768 18.1962 4.94768C17.9047 4.52818 18.0712 3.72403 18.0712 3.72403C18.3131 3.52731 18.4064 3.00505 18.4064 3.00505C18.808 2.69711 18.7883 2.2465 18.6028 2.25139C18.4539 2.2553 18.4877 2.13202 18.4877 2.13202C18.7388 0.103572 16.938 0 16.938 0H16.6631C16.6631 0 14.8615 0.103572 15.1123 2.13153C15.1123 2.13153 15.146 2.25497 14.9958 2.2509C14.8108 2.24601 14.7936 2.69662 15.1937 3.00457C15.1937 3.00457 15.2869 3.52666 15.5289 3.72354C15.5289 3.72354 15.6954 4.52769 15.4039 4.94719C15.4039 4.94719 15.2131 5.39177 13.8362 5.59663C13.8362 5.59663 12.4723 5.60999 12.2869 7.45784L12.2611 10.1668C12.2611 10.1668 12.1619 11.7714 11.6224 12.6316C11.6224 12.6316 11.3096 13.1839 11.2734 14.4551C11.2734 14.4551 11.1818 15.5325 11.0612 15.8677C10.9878 16.0646 10.7321 16.3978 10.5322 16.6476C10.14 17.135 9.48096 18.1523 9.83353 18.2684C9.83353 18.2684 10.1086 18.2951 10.4701 17.5752C10.4701 17.5752 10.4626 17.8509 10.1726 18.654C10.1156 18.8029 9.8731 19.5644 10.2659 19.2906C10.2659 19.2906 10.4478 19.1657 10.6843 18.3824C10.6843 18.3824 10.5574 19.7013 10.6986 19.7676C10.8831 19.858 10.9837 19.5971 11.0669 18.4428C11.0669 18.4428 11.1523 18.0701 11.1919 19.4929C11.1938 19.5663 11.3015 19.9322 11.4518 19.6199C11.5786 19.358 11.5216 18.6649 11.5216 18.4319C11.5216 18.4319 11.6843 19.3312 11.8364 19.3312C11.8364 19.3312 12.0154 19.5414 11.9419 18.4239C11.93 18.2414 11.994 17.8667 12.002 17.7576L12.0193 17.3291C12.0193 17.3291 11.9747 16.8383 11.9747 16.6339C11.9747 16.5832 12.1502 15.9149 12.6132 15.2095C12.6132 15.2095 13.5761 13.5015 13.5141 12.3853C13.5141 12.3853 13.5007 11.3203 13.8885 10.7169C13.8885 10.7169 14.1637 13.7339 13.9772 14.5809C13.9772 14.5809 13.111 16.6617 13.3021 18.2137C13.4428 19.3689 13.7141 21.835 13.9903 22.8088C14.1345 23.3127 14.0492 24.5837 14.1648 24.9011C14.2169 25.039 14.1896 25.166 14.0775 25.4784C13.6888 26.5711 13.7383 27.3407 14.7161 30.2401C14.7161 30.2401 15.017 30.8825 14.8649 32.0368C14.8649 32.0368 14.2402 33.322 14.6403 33.3477C14.6403 33.3477 14.6716 33.4329 14.8069 33.3655C14.8069 33.3655 15.023 33.5886 15.2566 33.4676C15.2566 33.4676 15.4728 33.6402 15.6577 33.4865C15.6577 33.4865 15.8059 33.657 16.022 33.5192C16.022 33.5192 16.3056 33.7106 16.4742 33.5033C16.4742 33.5033 16.7737 33.5697 16.2405 32.219C16.2405 32.219 16.0363 30.7871 15.9238 30.5055C15.7091 29.9722 15.8607 28.5105 15.9065 28.1972C15.9803 27.6734 15.9407 26.7812 15.8059 26.0909C15.7072 25.5961 15.9744 24.662 16.0731 24.0878C16.2758 22.879 16.6732 19.793 16.6235 19.209L16.7877 19.2667C16.9053 19.267 16.9795 19.2095 16.9795 19.2095Z" fill="#2E3039"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1_27024">
|
||||
<rect width="33.6" height="33.6" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
@ -0,0 +1,3 @@
|
||||
<svg width="29" height="29" viewBox="0 0 29 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.3286 2.69938C20.35 2.69897 20.3714 2.69933 20.3928 2.70045C23.661 2.87123 26.0252 4.18642 26.9215 6.63726C27.7812 8.98782 27.1501 12.0533 25.3157 15.4357C25.0787 15.8726 24.5324 16.0347 24.0955 15.7977C23.6585 15.5608 23.4964 15.0145 23.7334 14.5775C25.4808 11.3556 25.8242 8.87752 25.231 7.25551C24.6766 5.73949 23.1698 4.65557 20.3309 4.49971C16.1883 4.58993 11.818 6.83789 8.27071 10.4289C6.7718 11.9463 5.31466 13.7628 4.36882 15.6822C3.42279 17.602 3.02302 19.55 3.50569 21.3927C3.69277 22.1069 4.04066 22.589 4.67608 23.0939C5.57342 23.807 6.41967 24.1687 7.46019 24.2696C8.53543 24.3738 9.86963 24.2049 11.75 23.7446C15.4014 22.631 17.6813 20.9756 19.6272 19.2448C19.7876 19.0443 20.034 18.9097 20.331 18.9097C20.8232 18.9097 21.1638 19.272 21.2279 19.6827C21.7111 21.2628 21.3119 22.6974 20.5801 24.8969C20.4232 25.3685 19.9137 25.6237 19.442 25.4668C18.9704 25.3098 18.7153 24.8003 18.8722 24.3287C19.28 23.1028 19.5184 22.2748 19.6042 21.6215C17.7553 23.0989 15.4915 24.489 12.25 25.4739C12.2344 25.4786 12.2187 25.483 12.2029 25.4868C10.2524 25.9657 8.67373 26.1956 7.28654 26.0612C5.86012 25.9229 4.69201 25.4057 3.55624 24.5032C2.67855 23.8057 2.06862 23.0101 1.76444 21.8488C1.13472 19.4447 1.69385 17.0384 2.75421 14.8865C3.81477 12.7343 5.4115 10.7621 6.99014 9.16393C10.7681 5.33933 15.5731 2.79018 20.3286 2.69938ZM19.4866 20.1101C19.4865 20.11 19.4865 20.11 19.4865 20.1099L20.3284 19.8253L19.4866 20.1101ZM20.3284 19.8253L20.3223 19.8184L20.2584 19.7472M20.3284 19.8253L20.3223 19.8184Z" fill="#2E3039"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@ -0,0 +1,41 @@
|
||||
import 'package:hmg_patient_app_new/core/enums.dart';
|
||||
|
||||
class OrganModel {
|
||||
final String id;
|
||||
final String name;
|
||||
final BodyView bodyView;
|
||||
final OrganPosition position;
|
||||
|
||||
const OrganModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.bodyView,
|
||||
required this.position,
|
||||
});
|
||||
|
||||
OrganModel copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
BodyView? bodyView,
|
||||
OrganPosition? position,
|
||||
}) {
|
||||
return OrganModel(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
bodyView: bodyView ?? this.bodyView,
|
||||
position: position ?? this.position,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class OrganPosition {
|
||||
final double x; // Percentage from left (0.0 to 1.0)
|
||||
final double y; // Percentage from top (0.0 to 1.0)
|
||||
|
||||
const OrganPosition({
|
||||
required this.x,
|
||||
required this.y,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,371 @@
|
||||
import 'package:hmg_patient_app_new/core/enums.dart';
|
||||
import 'package:hmg_patient_app_new/features/symptoms_checker/models/organ_model.dart';
|
||||
|
||||
class OrganData {
|
||||
// ==================== FRONT VIEW ====================
|
||||
static const List<OrganModel> frontViewOrgans = [
|
||||
// ========== HEAD & FACE ==========
|
||||
OrganModel(
|
||||
id: 'head',
|
||||
name: 'Head',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.50, y: 0.03),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'left_eye',
|
||||
name: 'Left Eye',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.43, y: 0.07),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'right_eye',
|
||||
name: 'Right Eye',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.57, y: 0.07),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'nose_mouth',
|
||||
name: 'Nose/Mouth',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.50, y: 0.09),
|
||||
),
|
||||
|
||||
// ========== NECK ==========
|
||||
OrganModel(
|
||||
id: 'throat',
|
||||
name: 'Throat',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.50, y: 0.15),
|
||||
),
|
||||
|
||||
// ========== SHOULDERS ==========
|
||||
OrganModel(
|
||||
id: 'left_shoulder',
|
||||
name: 'Left Shoulder',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.22, y: 0.22),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'right_shoulder',
|
||||
name: 'Right Shoulder',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.8, y: 0.22),
|
||||
),
|
||||
|
||||
// ========== CHEST ==========
|
||||
OrganModel(
|
||||
id: 'left_chest',
|
||||
name: 'Left Chest',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.38, y: 0.22),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'center_chest',
|
||||
name: 'Center Chest',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.52, y: 0.25),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'right_chest',
|
||||
name: 'Right Chest',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.65, y: 0.22),
|
||||
),
|
||||
|
||||
// ========== RIBS ==========
|
||||
OrganModel(
|
||||
id: 'left_ribs',
|
||||
name: 'Left Ribs',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.38, y: 0.35),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'right_ribs',
|
||||
name: 'Right Ribs',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.64, y: 0.35),
|
||||
),
|
||||
|
||||
// ========== ABDOMEN ==========
|
||||
OrganModel(
|
||||
id: 'upper_abdomen',
|
||||
name: 'Upper Abdomen',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.51, y: 0.31),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'navel',
|
||||
name: 'Navel',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.51, y: 0.38),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'lower_abdomen',
|
||||
name: 'Lower Abdomen',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.51, y: 0.44),
|
||||
),
|
||||
|
||||
// ========== PELVIS ==========
|
||||
OrganModel(
|
||||
id: 'left_groin',
|
||||
name: 'Left Groin',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.42, y: 0.49),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'right_groin',
|
||||
name: 'Right Groin',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.59, y: 0.49),
|
||||
),
|
||||
|
||||
// ========== LEFT ARM ==========
|
||||
|
||||
OrganModel(
|
||||
id: 'left_elbow',
|
||||
name: 'Left Elbow',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.21, y: 0.35),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'left_forearm',
|
||||
name: 'Left Forearm',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.19, y: 0.43),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'left_wrist',
|
||||
name: 'Left Wrist',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.14, y: 0.5),
|
||||
),
|
||||
|
||||
// ========== RIGHT ARM ==========
|
||||
|
||||
OrganModel(
|
||||
id: 'right_elbow',
|
||||
name: 'Right Elbow',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.79, y: 0.35),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'right_forearm',
|
||||
name: 'Right Forearm',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.81, y: 0.43),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'right_wrist',
|
||||
name: 'Right Wrist',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.85, y: 0.5),
|
||||
),
|
||||
|
||||
// ========== LEFT LEG ==========
|
||||
OrganModel(
|
||||
id: 'left_thigh',
|
||||
name: 'Left Thigh',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.4, y: 0.60),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'left_knee',
|
||||
name: 'Left Knee',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.4, y: 0.72),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'left_shin',
|
||||
name: 'Left Shin',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.41, y: 0.82),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'left_ankle',
|
||||
name: 'Left Ankle',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.43, y: 0.91),
|
||||
),
|
||||
|
||||
// ========== RIGHT LEG ==========
|
||||
OrganModel(
|
||||
id: 'right_thigh',
|
||||
name: 'Right Thigh',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.65, y: 0.60),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'right_knee',
|
||||
name: 'Right Knee',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.6, y: 0.72),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'right_shin',
|
||||
name: 'Right Shin',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.57, y: 0.82),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'right_ankle',
|
||||
name: 'Right Ankle',
|
||||
bodyView: BodyView.front,
|
||||
position: OrganPosition(x: 0.57, y: 0.91),
|
||||
),
|
||||
];
|
||||
|
||||
// ==================== BACK VIEW ====================
|
||||
static const List<OrganModel> backViewOrgans = [
|
||||
// ========== HEAD & NECK ==========
|
||||
OrganModel(
|
||||
id: 'back_of_head',
|
||||
name: 'Back of Head',
|
||||
bodyView: BodyView.back,
|
||||
position: OrganPosition(x: 0.50, y: 0.04),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'neck_back',
|
||||
name: 'Neck',
|
||||
bodyView: BodyView.back,
|
||||
position: OrganPosition(x: 0.50, y: 0.14),
|
||||
),
|
||||
|
||||
// ========== SHOULDERS ==========
|
||||
OrganModel(
|
||||
id: 'left_shoulder_back',
|
||||
name: 'Left Shoulder',
|
||||
bodyView: BodyView.back,
|
||||
position: OrganPosition(x: 0.33, y: 0.19),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'right_shoulder_back',
|
||||
name: 'Right Shoulder',
|
||||
bodyView: BodyView.back,
|
||||
position: OrganPosition(x: 0.67, y: 0.19),
|
||||
),
|
||||
|
||||
// ========== UPPER BACK ==========
|
||||
OrganModel(
|
||||
id: 'upper_spine',
|
||||
name: 'Upper Spine',
|
||||
bodyView: BodyView.back,
|
||||
position: OrganPosition(x: 0.50, y: 0.23),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'left_upper_back',
|
||||
name: 'Left Upper Back',
|
||||
bodyView: BodyView.back,
|
||||
position: OrganPosition(x: 0.38, y: 0.28),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'right_upper_back',
|
||||
name: 'Right Upper Back',
|
||||
bodyView: BodyView.back,
|
||||
position: OrganPosition(x: 0.62, y: 0.28),
|
||||
),
|
||||
|
||||
// ========== MID BACK ==========
|
||||
OrganModel(
|
||||
id: 'mid_spine',
|
||||
name: 'Mid Spine',
|
||||
bodyView: BodyView.back,
|
||||
position: OrganPosition(x: 0.50, y: 0.35),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'left_mid_back',
|
||||
name: 'Left Mid Back',
|
||||
bodyView: BodyView.back,
|
||||
position: OrganPosition(x: 0.38, y: 0.35),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'right_mid_back',
|
||||
name: 'Right Mid Back',
|
||||
bodyView: BodyView.back,
|
||||
position: OrganPosition(x: 0.62, y: 0.35),
|
||||
),
|
||||
|
||||
// ========== LOWER BACK ==========
|
||||
OrganModel(
|
||||
id: 'lower_spine',
|
||||
name: 'Lower Back',
|
||||
bodyView: BodyView.back,
|
||||
position: OrganPosition(x: 0.50, y: 0.43),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'left_lower_back',
|
||||
name: 'Left Lower Back',
|
||||
bodyView: BodyView.back,
|
||||
position: OrganPosition(x: 0.40, y: 0.43),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'right_lower_back',
|
||||
name: 'Right Lower Back',
|
||||
bodyView: BodyView.back,
|
||||
position: OrganPosition(x: 0.60, y: 0.43),
|
||||
),
|
||||
|
||||
// ========== GLUTES ==========
|
||||
OrganModel(
|
||||
id: 'sacrum',
|
||||
name: 'Sacrum',
|
||||
bodyView: BodyView.back,
|
||||
position: OrganPosition(x: 0.50, y: 0.52),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'left_glute',
|
||||
name: 'Left Glute',
|
||||
bodyView: BodyView.back,
|
||||
position: OrganPosition(x: 0.45, y: 0.56),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'right_glute',
|
||||
name: 'Right Glute',
|
||||
bodyView: BodyView.back,
|
||||
position: OrganPosition(x: 0.55, y: 0.56),
|
||||
),
|
||||
|
||||
// ========== LEGS ==========
|
||||
OrganModel(
|
||||
id: 'left_hamstring',
|
||||
name: 'Left Hamstring',
|
||||
bodyView: BodyView.back,
|
||||
position: OrganPosition(x: 0.43, y: 0.65),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'right_hamstring',
|
||||
name: 'Right Hamstring',
|
||||
bodyView: BodyView.back,
|
||||
position: OrganPosition(x: 0.57, y: 0.65),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'left_calf',
|
||||
name: 'Left Calf',
|
||||
bodyView: BodyView.back,
|
||||
position: OrganPosition(x: 0.43, y: 0.79),
|
||||
),
|
||||
OrganModel(
|
||||
id: 'right_calf',
|
||||
name: 'Right Calf',
|
||||
bodyView: BodyView.back,
|
||||
position: OrganPosition(x: 0.57, y: 0.79),
|
||||
),
|
||||
];
|
||||
|
||||
// Helper methods
|
||||
static List<OrganModel> getOrgansForView(BodyView view) {
|
||||
return view == BodyView.front ? frontViewOrgans : backViewOrgans;
|
||||
}
|
||||
|
||||
static List<OrganModel> getAllOrgans() {
|
||||
return [...frontViewOrgans, ...backViewOrgans];
|
||||
}
|
||||
|
||||
static OrganModel? getOrganById(String id) {
|
||||
try {
|
||||
return getAllOrgans().firstWhere((organ) => organ.id == id);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,207 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_assets.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_export.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_state.dart';
|
||||
import 'package:hmg_patient_app_new/core/dependencies.dart';
|
||||
import 'package:hmg_patient_app_new/core/utils/date_util.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/int_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart';
|
||||
import 'package:hmg_patient_app_new/features/medical_file/models/family_file_response_model.dart';
|
||||
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
|
||||
import 'package:hmg_patient_app_new/theme/colors.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class FamilyCardWidget extends StatelessWidget {
|
||||
final Function() onAddFamilyMemberPress;
|
||||
final Function(FamilyFileResponseModelLists member) onFamilySwitchPress;
|
||||
final FamilyFileResponseModelLists profile;
|
||||
|
||||
const FamilyCardWidget({
|
||||
super.key,
|
||||
required this.onAddFamilyMemberPress,
|
||||
required this.profile,
|
||||
required this.onFamilySwitchPress(FamilyFileResponseModelLists member),
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
AppState appState = getIt.get<AppState>();
|
||||
final isActive = (profile.responseId == appState.getAuthenticatedUser()?.patientId);
|
||||
final isParentUser = appState.getAuthenticatedUser()?.isParentUser ?? false;
|
||||
final canSwitch = isParentUser || (!isParentUser && profile.responseId == appState.getSuperUserID);
|
||||
return Container(
|
||||
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||
color: AppColors.whiteColor,
|
||||
borderRadius: 24.r,
|
||||
hasShadow: true,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 8.h,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Image.asset(profile.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg, width: 56.w, height: 56.h),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
"${profile.patientName}".toText18(isBold: true, weight: FontWeight.w600, textOverflow: TextOverflow.ellipsis, maxlines: 1),
|
||||
AppCustomChipWidget(
|
||||
icon: AppAssets.file_icon,
|
||||
labelText: "${LocaleKeys.fileNo.tr(context: context)}: ${profile.responseId}",
|
||||
iconSize: 12.w,
|
||||
),
|
||||
],
|
||||
).expanded,
|
||||
Icon(Icons.qr_code, size: 56.h)
|
||||
],
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
SizedBox(
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
spacing: 4.w,
|
||||
runSpacing: 4.h,
|
||||
children: [
|
||||
AppCustomChipWidget(
|
||||
labelText: "${profile.age} Years Old".needTranslation,
|
||||
),
|
||||
isActive && appState.getAuthenticatedUser()!.bloodGroup != null
|
||||
? AppCustomChipWidget(
|
||||
icon: AppAssets.blood_icon,
|
||||
labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 8.w),
|
||||
labelText: "Blood: ${appState.getAuthenticatedUser()!.bloodGroup ?? ""}",
|
||||
iconColor: AppColors.primaryRedColor)
|
||||
: SizedBox(),
|
||||
Selector<InsuranceViewModel, ({bool isEmpty, int? patientID, bool isLoading, String? cardValidTo})>(
|
||||
selector: (context, insuranceVM) => (
|
||||
isEmpty: insuranceVM.patientInsuranceList.isEmpty,
|
||||
patientID: insuranceVM.patientInsuranceList.isNotEmpty ? insuranceVM.patientInsuranceList.first.patientID : null,
|
||||
isLoading: insuranceVM.isInsuranceLoading,
|
||||
cardValidTo: insuranceVM.patientInsuranceList.isNotEmpty ? insuranceVM.patientInsuranceList.first.cardValidTo : null
|
||||
),
|
||||
builder: (context, data, child) {
|
||||
if (data.isEmpty) {
|
||||
return const SizedBox();
|
||||
} else if (profile.responseId != data.patientID) {
|
||||
return SizedBox();
|
||||
}
|
||||
|
||||
final isLoading = data.isLoading;
|
||||
final isExpired = !isLoading && DateTime.now().isAfter(DateUtil.convertStringToDate(data.cardValidTo));
|
||||
|
||||
final String icon;
|
||||
final String labelText;
|
||||
final Color iconColor;
|
||||
final Color backgroundColor;
|
||||
|
||||
if (isLoading) {
|
||||
icon = AppAssets.cancel_circle_icon;
|
||||
labelText = "Insurance".needTranslation;
|
||||
iconColor = AppColors.primaryRedColor;
|
||||
backgroundColor = AppColors.primaryRedColor;
|
||||
} else if (isExpired) {
|
||||
icon = AppAssets.cancel_circle_icon;
|
||||
labelText = "Insurance Expired".needTranslation;
|
||||
iconColor = AppColors.primaryRedColor;
|
||||
backgroundColor = AppColors.primaryRedColor.withValues(alpha: 0.15);
|
||||
} else {
|
||||
icon = AppAssets.insurance_active_icon;
|
||||
labelText = "Insurance Active".needTranslation;
|
||||
iconColor = AppColors.successColor;
|
||||
backgroundColor = AppColors.successColor.withValues(alpha: 0.15);
|
||||
}
|
||||
|
||||
return AppCustomChipWidget(
|
||||
icon: icon,
|
||||
labelText: labelText,
|
||||
iconColor: iconColor,
|
||||
iconSize: 12.w,
|
||||
backgroundColor: backgroundColor,
|
||||
labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 8.w),
|
||||
// padding: EdgeInsets.zero,
|
||||
).toShimmer2(isShow: isLoading);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingOnly(top: 16.h, right: 16.w, left: 16.w, bottom: 12.h),
|
||||
1.divider,
|
||||
_buildActionButton(appState),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButton(AppState appState) {
|
||||
final isParentUser = appState.getAuthenticatedUser()?.isParentUser ?? false;
|
||||
final int? currentUserId = appState.getAuthenticatedUser()?.patientId;
|
||||
final int? superUserId = appState.getSuperUserID;
|
||||
|
||||
if (isParentUser) {
|
||||
return _buildParentUserButton(currentUserId);
|
||||
} else {
|
||||
return _buildNonParentUserButton(superUserId);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildParentUserButton(int? currentUserId) {
|
||||
final canSwitch = profile.responseId != currentUserId;
|
||||
|
||||
return CustomButton(
|
||||
icon: canSwitch ? AppAssets.switch_user : AppAssets.add_family,
|
||||
text: canSwitch ? "Switch Family File".needTranslation : "Add a new family member".needTranslation,
|
||||
onPressed: canSwitch ? () => onFamilySwitchPress(profile) : onAddFamilyMemberPress,
|
||||
backgroundColor: canSwitch ? AppColors.secondaryLightRedColor : AppColors.primaryRedColor,
|
||||
borderColor: canSwitch ? AppColors.secondaryLightRedColor : AppColors.primaryRedColor,
|
||||
textColor: canSwitch ? AppColors.primaryRedColor : AppColors.whiteColor,
|
||||
iconColor: canSwitch ? AppColors.primaryRedColor : AppColors.whiteColor,
|
||||
height: isFoldable ? 50.h : 40.h,
|
||||
fontSize: 14.f,
|
||||
).paddingOnly(top: 12.h, right: 16.w, left: 16.w, bottom: 16.h);
|
||||
}
|
||||
|
||||
Widget _buildNonParentUserButton(int? superUserId) {
|
||||
final canSwitchBack = superUserId != null && superUserId == profile.responseId;
|
||||
|
||||
return CustomButton(
|
||||
icon: AppAssets.switch_user,
|
||||
text: canSwitchBack ? "Switch Back To Family File".needTranslation : "Switch".needTranslation,
|
||||
backgroundColor: canSwitchBack ? AppColors.primaryRedColor : Colors.grey.shade200,
|
||||
borderColor: canSwitchBack ? AppColors.primaryRedColor : Colors.grey.shade200,
|
||||
textColor: canSwitchBack ? AppColors.whiteColor : AppColors.greyTextColor,
|
||||
iconColor: canSwitchBack ? AppColors.whiteColor : AppColors.greyTextColor,
|
||||
onPressed: canSwitchBack ? () => onFamilySwitchPress(profile) : () {},
|
||||
height: isFoldable ? 50.h : 40.h,
|
||||
fontSize: 14.f,
|
||||
).paddingOnly(top: 12.h, right: 16.w, left: 16.w, bottom: 16.h);
|
||||
}
|
||||
|
||||
// //TODO: Add family file switch logic here
|
||||
// isRootUser
|
||||
// ? CustomButton(icon: AppAssets.add_family, text: "Add a new family member".needTranslation, height: 40.h, fontSize: 14, onPressed: () {})
|
||||
// .paddingOnly(top: 12, right: 16, left: 16, bottom: 16)
|
||||
// : CustomButton(
|
||||
// icon: AppAssets.add_family,
|
||||
// backgroundColor: AppColors.secondaryLightRedColor,
|
||||
// borderColor: AppColors.secondaryLightRedColor,
|
||||
// textColor: AppColors.primaryRedColor,
|
||||
// iconColor: AppColors.primaryRedColor,
|
||||
// text: "Switch to this medical file".needTranslation,
|
||||
// height: 40.h,
|
||||
// fontSize: 14,
|
||||
// onPressed: () {})
|
||||
// .paddingOnly(top: 12, right: 16, left: 16, bottom: 16),
|
||||
//
|
||||
}
|
||||
@ -0,0 +1,337 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_assets.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_export.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_state.dart';
|
||||
import 'package:hmg_patient_app_new/core/dependencies.dart';
|
||||
import 'package:hmg_patient_app_new/core/enums.dart';
|
||||
import 'package:hmg_patient_app_new/core/utils/utils.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/presentation/symptoms_checker/symptoms_checker_view_model.dart';
|
||||
import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/interactive_body_widget.dart';
|
||||
import 'package:hmg_patient_app_new/theme/colors.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class OrganSelectorPage extends StatefulWidget {
|
||||
const OrganSelectorPage({super.key});
|
||||
|
||||
@override
|
||||
State<OrganSelectorPage> createState() => _OrganSelectorPageState();
|
||||
}
|
||||
|
||||
class _OrganSelectorPageState extends State<OrganSelectorPage> {
|
||||
late final AppState _appState;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_appState = getIt.get<AppState>();
|
||||
}
|
||||
|
||||
void _onNextPressed(SymptomsCheckerViewModel viewModel) {
|
||||
if (!viewModel.validateSelection()) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Please select at least one organ'.needTranslation),
|
||||
backgroundColor: AppColors.errorColor,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Selected organs:\n\n ${viewModel.getSelectedOrganIds()}'),
|
||||
backgroundColor: AppColors.successColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => SymptomsCheckerViewModel(),
|
||||
child: Scaffold(
|
||||
backgroundColor: AppColors.bgScaffoldColor,
|
||||
appBar: _buildAppBar(),
|
||||
body: Consumer<SymptomsCheckerViewModel>(
|
||||
builder: (context, viewModel, _) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildTitle(),
|
||||
SizedBox(height: 8.h),
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildBodyViewer(viewModel),
|
||||
_buildViewToggleButtons(viewModel),
|
||||
_buildBottomSheet(viewModel),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
PreferredSizeWidget _buildAppBar() {
|
||||
return AppBar(
|
||||
backgroundColor: AppColors.bgScaffoldColor,
|
||||
elevation: 0,
|
||||
leading: Transform.flip(
|
||||
flipX: _appState.isArabic(),
|
||||
child: IconButton(
|
||||
icon: Utils.buildSvgWithAssets(
|
||||
icon: AppAssets.arrow_back,
|
||||
width: 32.h,
|
||||
height: 32.h,
|
||||
),
|
||||
padding: const EdgeInsets.only(left: 12),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
highlightColor: Colors.transparent,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTitle() {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
child: Text(
|
||||
"Organ Selector".needTranslation,
|
||||
style: TextStyle(
|
||||
color: AppColors.textColor,
|
||||
fontSize: 22.f,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBodyViewer(SymptomsCheckerViewModel viewModel) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(16.h, 16.h, 16.h, 45.h),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
transitionBuilder: (child, animation) => _build3DFlipTransition(child, animation),
|
||||
switchInCurve: Curves.easeInOut,
|
||||
switchOutCurve: Curves.easeInOut,
|
||||
child: InteractiveBodyWidget(
|
||||
key: ValueKey<BodyView>(viewModel.currentView),
|
||||
bodyImageAsset: viewModel.currentView == BodyView.front ? AppAssets.fullBodyFront : AppAssets.fullBodyBack,
|
||||
organs: viewModel.currentOrgans,
|
||||
selectedOrganIds: viewModel.selectedOrganIds,
|
||||
onOrganTap: viewModel.toggleOrganSelection,
|
||||
isBodyHidden: viewModel.isBodyHidden,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _build3DFlipTransition(Widget child, Animation<double> animation) {
|
||||
final rotateAnimation = Tween<double>(begin: 0.5, end: 0.0).animate(
|
||||
CurvedAnimation(parent: animation, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: rotateAnimation,
|
||||
builder: (context, child) {
|
||||
final angle = rotateAnimation.value * 3.14159;
|
||||
final transform = Matrix4.identity()
|
||||
..setEntry(3, 2, 0.001)
|
||||
..rotateY(angle);
|
||||
|
||||
return Transform(
|
||||
transform: transform,
|
||||
alignment: Alignment.center,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildViewToggleButtons(SymptomsCheckerViewModel viewModel) {
|
||||
return Positioned(
|
||||
left: 16.w,
|
||||
top: 36.h,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildToggleButton(
|
||||
icon: AppAssets.bodyIcon,
|
||||
onTap: viewModel.toggleIsBodyHidden,
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
_buildToggleButton(
|
||||
icon: AppAssets.rotateIcon,
|
||||
onTap: viewModel.toggleView,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildToggleButton({required String icon, required VoidCallback onTap}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 48.w,
|
||||
height: 48.h,
|
||||
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||
color: AppColors.whiteColor,
|
||||
borderRadius: 11.r,
|
||||
),
|
||||
child: Center(
|
||||
child: Utils.buildSvgWithAssets(
|
||||
icon: icon,
|
||||
width: 25.w,
|
||||
height: 25.h,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomSheet(SymptomsCheckerViewModel viewModel) {
|
||||
return Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (viewModel.isBottomSheetExpanded) ...[
|
||||
SizedBox(height: 20.h),
|
||||
_buildBottomSheetTitle(),
|
||||
SizedBox(height: 12.h),
|
||||
_buildSelectedOrgansContent(viewModel),
|
||||
SizedBox(height: 16.h),
|
||||
_buildNextButton(viewModel),
|
||||
SizedBox(height: 16.h),
|
||||
] else
|
||||
SizedBox(height: 20.h),
|
||||
],
|
||||
),
|
||||
_buildExpandCollapseButton(viewModel),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomSheetTitle() {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
child: Text(
|
||||
'Selected Organs'.needTranslation,
|
||||
style: TextStyle(
|
||||
fontSize: 16.f,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSelectedOrgansContent(SymptomsCheckerViewModel viewModel) {
|
||||
if (viewModel.selectedOrgans.isEmpty) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
child: Text(
|
||||
'No organs selected yet'.needTranslation,
|
||||
style: TextStyle(
|
||||
color: AppColors.greyTextColor,
|
||||
fontSize: 14.f,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: 150.h),
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
child: Wrap(
|
||||
spacing: 8.w,
|
||||
runSpacing: 8.h,
|
||||
children: viewModel.selectedOrgans.map((organ) {
|
||||
return AppCustomChipWidget(
|
||||
labelText: organ.name,
|
||||
backgroundColor: AppColors.secondaryLightRedColor,
|
||||
textColor: AppColors.primaryRedColor,
|
||||
deleteIcon: AppAssets.cancel,
|
||||
deleteIconColor: AppColors.primaryRedColor,
|
||||
deleteIconHasColor: false,
|
||||
onDeleteTap: () => viewModel.removeOrgan(organ.id),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNextButton(SymptomsCheckerViewModel viewModel) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
child: CustomButton(
|
||||
text: 'Next'.needTranslation,
|
||||
onPressed: () => _onNextPressed(viewModel),
|
||||
isDisabled: viewModel.selectedOrgans.isEmpty,
|
||||
backgroundColor: AppColors.primaryRedColor,
|
||||
textColor: AppColors.whiteColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildExpandCollapseButton(SymptomsCheckerViewModel viewModel) {
|
||||
return Positioned(
|
||||
right: 24.w,
|
||||
top: -24.h,
|
||||
child: GestureDetector(
|
||||
onTap: viewModel.toggleBottomSheet,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
width: 70.w,
|
||||
height: 70.h,
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
width: 48.w,
|
||||
height: 48.h,
|
||||
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||
color: AppColors.whiteColor,
|
||||
borderRadius: 11.r,
|
||||
),
|
||||
child: Center(
|
||||
child: AnimatedRotation(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
turns: viewModel.isBottomSheetExpanded ? 0.25 : -0.25,
|
||||
child: Utils.buildSvgWithAssets(
|
||||
icon: AppAssets.arrowRight,
|
||||
width: 25.w,
|
||||
height: 25.h,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,108 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:hmg_patient_app_new/core/enums.dart';
|
||||
import 'package:hmg_patient_app_new/features/symptoms_checker/models/organ_model.dart';
|
||||
import 'package:hmg_patient_app_new/features/symptoms_checker/organ_mapping_data.dart';
|
||||
|
||||
class SymptomsCheckerViewModel extends ChangeNotifier {
|
||||
// State variables
|
||||
bool isBodyHidden = false;
|
||||
BodyView _currentView = BodyView.front;
|
||||
final Set<String> _selectedOrganIds = {};
|
||||
bool _isBottomSheetExpanded = false;
|
||||
|
||||
// Getters
|
||||
|
||||
BodyView get currentView => _currentView;
|
||||
|
||||
Set<String> get selectedOrganIds => _selectedOrganIds;
|
||||
|
||||
bool get isBottomSheetExpanded => _isBottomSheetExpanded;
|
||||
|
||||
/// Get organs for current view
|
||||
List<OrganModel> get currentOrgans => OrganData.getOrgansForView(_currentView);
|
||||
|
||||
/// Get all selected organs from both views
|
||||
List<OrganModel> get selectedOrgans {
|
||||
final allOrgans = [
|
||||
...OrganData.frontViewOrgans,
|
||||
...OrganData.backViewOrgans,
|
||||
];
|
||||
return allOrgans.where((organ) => _selectedOrganIds.contains(organ.id)).toList();
|
||||
}
|
||||
|
||||
/// Check if any organs are selected
|
||||
bool get hasSelectedOrgans => _selectedOrganIds.isNotEmpty;
|
||||
|
||||
/// Get count of selected organs
|
||||
int get selectedOrgansCount => _selectedOrganIds.length;
|
||||
|
||||
// Methods
|
||||
|
||||
/// Toggle between front and back body view
|
||||
void toggleView() {
|
||||
_currentView = _currentView == BodyView.front ? BodyView.back : BodyView.front;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
toggleIsBodyHidden() {
|
||||
isBodyHidden = !isBodyHidden;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Toggle organ selection (add if not selected, remove if selected)
|
||||
void toggleOrganSelection(String organId) {
|
||||
if (_selectedOrganIds.contains(organId)) {
|
||||
_selectedOrganIds.remove(organId);
|
||||
} else {
|
||||
_selectedOrganIds.add(organId);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Remove a specific organ from selection
|
||||
void removeOrgan(String organId) {
|
||||
_selectedOrganIds.remove(organId);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Clear all selected organs
|
||||
void clearAllSelections() {
|
||||
_selectedOrganIds.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Toggle bottom sheet expanded/collapsed state
|
||||
void toggleBottomSheet() {
|
||||
_isBottomSheetExpanded = !_isBottomSheetExpanded;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Set bottom sheet expanded state
|
||||
void setBottomSheetExpanded(bool isExpanded) {
|
||||
_isBottomSheetExpanded = isExpanded;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Validate if at least one organ is selected
|
||||
bool validateSelection() {
|
||||
return _selectedOrganIds.isNotEmpty;
|
||||
}
|
||||
|
||||
/// Get selected organ IDs as a list
|
||||
List<String> getSelectedOrganIds() {
|
||||
return _selectedOrganIds.toList();
|
||||
}
|
||||
|
||||
/// Get selected organ names as a list
|
||||
List<String> getSelectedOrganNames() {
|
||||
return selectedOrgans.map((organ) => organ.name).toList();
|
||||
}
|
||||
|
||||
/// Reset the view model to initial state
|
||||
void reset() {
|
||||
_currentView = BodyView.front;
|
||||
_selectedOrganIds.clear();
|
||||
_isBottomSheetExpanded = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,121 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hmg_patient_app_new/features/symptoms_checker/models/organ_model.dart';
|
||||
import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/organ_dot_widget.dart';
|
||||
|
||||
class InteractiveBodyWidget extends StatefulWidget {
|
||||
final String bodyImageAsset;
|
||||
final List<OrganModel> organs;
|
||||
final Set<String> selectedOrganIds;
|
||||
final bool isBodyHidden;
|
||||
final Function(String organId) onOrganTap;
|
||||
|
||||
const InteractiveBodyWidget({
|
||||
super.key,
|
||||
required this.bodyImageAsset,
|
||||
required this.organs,
|
||||
required this.selectedOrganIds,
|
||||
required this.onOrganTap,
|
||||
this.isBodyHidden = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<InteractiveBodyWidget> createState() => _InteractiveBodyWidgetState();
|
||||
}
|
||||
|
||||
class _InteractiveBodyWidgetState extends State<InteractiveBodyWidget> {
|
||||
double? _imageAspectRatio;
|
||||
final TransformationController _transformationController = TransformationController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadImageAspectRatio();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(InteractiveBodyWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.bodyImageAsset != widget.bodyImageAsset) {
|
||||
_loadImageAspectRatio();
|
||||
// Reset zoom when switching views
|
||||
_transformationController.value = Matrix4.identity();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_transformationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadImageAspectRatio() async {
|
||||
final ByteData data = await rootBundle.load(widget.bodyImageAsset);
|
||||
final ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
|
||||
final ui.FrameInfo frameInfo = await codec.getNextFrame();
|
||||
final image = frameInfo.image;
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_imageAspectRatio = image.width / image.height;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_imageAspectRatio == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Center(
|
||||
child: InteractiveViewer(
|
||||
transformationController: _transformationController,
|
||||
minScale: 0.5,
|
||||
maxScale: 4.0,
|
||||
boundaryMargin: const EdgeInsets.all(20),
|
||||
clipBehavior: Clip.none,
|
||||
child: AspectRatio(
|
||||
aspectRatio: _imageAspectRatio!,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, imageConstraints) {
|
||||
return Stack(
|
||||
children: [
|
||||
// Body image
|
||||
Positioned.fill(
|
||||
child: Image.asset(
|
||||
widget.bodyImageAsset,
|
||||
fit: BoxFit.fill,
|
||||
color: widget.isBodyHidden ? Colors.black.withValues(alpha: 0.5) : null,
|
||||
),
|
||||
),
|
||||
|
||||
// Organ dots
|
||||
...widget.organs.map((organ) {
|
||||
final isSelected = widget.selectedOrganIds.contains(organ.id);
|
||||
final dotSize = 16.0;
|
||||
|
||||
final leftPos = (organ.position.x * imageConstraints.maxWidth) - (dotSize / 2);
|
||||
final topPos = (organ.position.y * imageConstraints.maxHeight) - (dotSize / 2);
|
||||
|
||||
return Positioned(
|
||||
left: leftPos,
|
||||
top: topPos,
|
||||
child: OrganDot(isSelected: isSelected, onTap: () => widget.onOrganTap(organ.id), size: dotSize),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_export.dart';
|
||||
import 'package:hmg_patient_app_new/theme/colors.dart';
|
||||
|
||||
class OrganDot extends StatelessWidget {
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
final double size;
|
||||
|
||||
const OrganDot({super.key, required this.isSelected, required this.onTap, this.size = 50.0});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeInOut,
|
||||
width: size.w,
|
||||
height: size.h,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: isSelected ? AppColors.whiteColor : AppColors.blackColor.withValues(alpha: 0.6),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(isSelected ? 2.w : 3.w),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: isSelected ? AppColors.primaryRedColor : AppColors.whiteColor,
|
||||
),
|
||||
child: isSelected
|
||||
? Center(
|
||||
child: Container(
|
||||
width: size.w * 0.25,
|
||||
height: size.h * 0.25,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AppColors.whiteColor,
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,133 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_assets.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_export.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/features/symptoms_checker/models/organ_model.dart';
|
||||
import 'package:hmg_patient_app_new/theme/colors.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
|
||||
|
||||
class SelectedOrgansSection extends StatefulWidget {
|
||||
final List<OrganModel> selectedOrgans;
|
||||
final Function(String organId) onRemoveOrgan;
|
||||
|
||||
const SelectedOrgansSection({
|
||||
super.key,
|
||||
required this.selectedOrgans,
|
||||
required this.onRemoveOrgan,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SelectedOrgansSection> createState() => _SelectedOrgansSectionState();
|
||||
}
|
||||
|
||||
class _SelectedOrgansSectionState extends State<SelectedOrgansSection> {
|
||||
bool _isExpanded = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(16.h),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.whiteColor,
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.blackColor.withValues(alpha: 0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_isExpanded = !_isExpanded;
|
||||
});
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Selected Organs'.needTranslation,
|
||||
style: TextStyle(
|
||||
fontSize: 16.f,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textColor,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
if (widget.selectedOrgans.isNotEmpty)
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 8.w,
|
||||
vertical: 4.h,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryRedColor,
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
),
|
||||
child: Text(
|
||||
'${widget.selectedOrgans.length}',
|
||||
style: TextStyle(
|
||||
color: AppColors.whiteColor,
|
||||
fontSize: 12.f,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
AnimatedRotation(
|
||||
turns: _isExpanded ? 0.5 : 0.0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: AppColors.textColorLight,
|
||||
size: 24.h,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_isExpanded) ...[
|
||||
SizedBox(height: 16.h),
|
||||
if (widget.selectedOrgans.isEmpty)
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.h),
|
||||
child: Text(
|
||||
'No organs selected yet'.needTranslation,
|
||||
style: TextStyle(
|
||||
color: AppColors.greyTextColor,
|
||||
fontSize: 14.f,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Wrap(
|
||||
spacing: 8.w,
|
||||
runSpacing: 8.h,
|
||||
children: widget.selectedOrgans.map((organ) {
|
||||
return AppCustomChipWidget(
|
||||
labelText: organ.name,
|
||||
backgroundColor: AppColors.secondaryLightRedColor,
|
||||
textColor: AppColors.primaryRedColor,
|
||||
deleteIcon: AppAssets.cross_circle,
|
||||
deleteIconColor: AppColors.primaryRedColor,
|
||||
deleteIconHasColor: true,
|
||||
onDeleteTap: () => widget.onRemoveOrgan(organ.id),
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue