[{"data":1,"prerenderedAt":506},["ShallowReactive",2],{"portfolio-medsync-dashboard":3,"all-portfolio":486},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":7,"category":9,"client":10,"timeline":11,"role":12,"date":13,"tags":14,"body":22,"_type":480,"_id":481,"_source":482,"_file":483,"_stem":484,"_extension":485},"\u002Fportfolio\u002Fmedsync-dashboard","portfolio",false,"","MedSync — ระบบจัดการคลินิกและนัดหมายผู้ป่วย","SaaS","MedSync Thailand","14 สัปดาห์","Full-stack development, System Architecture, UI\u002FUX","2024-06-20",[15,16,17,18,19,20,21],"Vue 3","Nuxt 3","TypeScript","Supabase","PostgreSQL","Tailwind CSS","Pinia",{"type":23,"children":24,"toc":474},"root",[25,33,39,44,57,77,87,105,111,116,135,393,398,454,468],{"type":26,"tag":27,"props":28,"children":30},"element","h2",{"id":29},"ภาพรวมโปรเจกต์",[31],{"type":32,"value":29},"text",{"type":26,"tag":34,"props":35,"children":36},"p",{},[37],{"type":32,"value":38},"MedSync เป็น SaaS platform สำหรับคลินิกทั่วไปและคลินิกเฉพาะทางขนาดเล็กถึงกลาง เป้าหมายหลักคือลดภาระงานธุรการ เช่น การจัดตารางนัด การบันทึกประวัติผู้ป่วย และการออกเอกสารทางการแพทย์ ที่ยังทำด้วย Excel และกระดาษในคลินิกส่วนใหญ่",{"type":26,"tag":27,"props":40,"children":42},{"id":41},"สถาปัตยกรรมที่เราเลือก",[43],{"type":32,"value":41},{"type":26,"tag":34,"props":45,"children":46},{},[47,49,55],{"type":32,"value":48},"เราเลือก ",{"type":26,"tag":50,"props":51,"children":52},"strong",{},[53],{"type":32,"value":54},"Nuxt 3 + Vue 3",{"type":32,"value":56}," เพราะ:",{"type":26,"tag":58,"props":59,"children":60},"ul",{},[61,67,72],{"type":26,"tag":62,"props":63,"children":64},"li",{},[65],{"type":32,"value":66},"Composition API ทำให้ reuse logic ระหว่าง components ได้ง่าย",{"type":26,"tag":62,"props":68,"children":69},{},[70],{"type":32,"value":71},"Built-in SSR สำหรับหน้า public (landing page, pricing)",{"type":26,"tag":62,"props":73,"children":74},{},[75],{"type":32,"value":76},"TypeScript support ดีเยี่ยม ซึ่งสำคัญมากสำหรับ healthcare data",{"type":26,"tag":34,"props":78,"children":79},{},[80,82,86],{"type":32,"value":81},"สำหรับ backend เลือก ",{"type":26,"tag":50,"props":83,"children":84},{},[85],{"type":32,"value":18},{"type":32,"value":56},{"type":26,"tag":58,"props":88,"children":89},{},[90,95,100],{"type":26,"tag":62,"props":91,"children":92},{},[93],{"type":32,"value":94},"Row Level Security ทำให้ isolate ข้อมูลระหว่างคลินิกได้ง่าย",{"type":26,"tag":62,"props":96,"children":97},{},[98],{"type":32,"value":99},"Realtime subscriptions สำหรับ live appointment board",{"type":26,"tag":62,"props":101,"children":102},{},[103],{"type":32,"value":104},"Auth ที่ครบ พร้อม JWT ที่ integrate กับ Vue ได้ตรงไปตรงมา",{"type":26,"tag":27,"props":106,"children":108},{"id":107},"ความท้าทายหลัก-realtime-appointment-board",[109],{"type":32,"value":110},"ความท้าทายหลัก: Realtime Appointment Board",{"type":26,"tag":34,"props":112,"children":113},{},[114],{"type":32,"value":115},"ฟีเจอร์ที่ยากที่สุดคือ appointment board ที่ต้องอัปเดต realtime เมื่อนัดถูกสร้าง เลื่อน หรือยกเลิก โดยไม่กระทบ performance ของหน้าอื่น",{"type":26,"tag":34,"props":117,"children":118},{},[119,121,126,128,133],{"type":32,"value":120},"เราใช้ ",{"type":26,"tag":50,"props":122,"children":123},{},[124],{"type":32,"value":125},"Supabase Realtime Channels",{"type":32,"value":127}," ร่วมกับ ",{"type":26,"tag":50,"props":129,"children":130},{},[131],{"type":32,"value":132},"Pinia store",{"type":32,"value":134}," ออกแบบให้ events จาก websocket ไป mutate store โดยตรง โดยไม่ต้อง refetch ข้อมูลทั้งหมด",{"type":26,"tag":136,"props":137,"children":141},"pre",{"className":138,"code":139,"language":140,"meta":7,"style":7},"language-typescript shiki shiki-themes github-light github-dark","\u002F\u002F composables\u002FuseAppointments.ts\nconst channel = supabase.channel('appointments')\n  .on('postgres_changes', {\n    event: '*',\n    schema: 'public',\n    table: 'appointments',\n    filter: `clinic_id=eq.${clinicId}`,\n  }, (payload) => {\n    appointmentStore.handleRealtimeEvent(payload)\n  })\n  .subscribe()\n","typescript",[142],{"type":26,"tag":143,"props":144,"children":145},"code",{"__ignoreMap":7},[146,158,207,235,254,272,289,317,347,366,375],{"type":26,"tag":147,"props":148,"children":151},"span",{"class":149,"line":150},"line",1,[152],{"type":26,"tag":147,"props":153,"children":155},{"style":154},"--shiki-default:#6A737D;--shiki-dark:#6A737D",[156],{"type":32,"value":157},"\u002F\u002F composables\u002FuseAppointments.ts\n",{"type":26,"tag":147,"props":159,"children":161},{"class":149,"line":160},2,[162,168,174,179,185,191,196,202],{"type":26,"tag":147,"props":163,"children":165},{"style":164},"--shiki-default:#D73A49;--shiki-dark:#F97583",[166],{"type":32,"value":167},"const",{"type":26,"tag":147,"props":169,"children":171},{"style":170},"--shiki-default:#005CC5;--shiki-dark:#79B8FF",[172],{"type":32,"value":173}," channel",{"type":26,"tag":147,"props":175,"children":176},{"style":164},[177],{"type":32,"value":178}," =",{"type":26,"tag":147,"props":180,"children":182},{"style":181},"--shiki-default:#24292E;--shiki-dark:#E1E4E8",[183],{"type":32,"value":184}," supabase.",{"type":26,"tag":147,"props":186,"children":188},{"style":187},"--shiki-default:#6F42C1;--shiki-dark:#B392F0",[189],{"type":32,"value":190},"channel",{"type":26,"tag":147,"props":192,"children":193},{"style":181},[194],{"type":32,"value":195},"(",{"type":26,"tag":147,"props":197,"children":199},{"style":198},"--shiki-default:#032F62;--shiki-dark:#9ECBFF",[200],{"type":32,"value":201},"'appointments'",{"type":26,"tag":147,"props":203,"children":204},{"style":181},[205],{"type":32,"value":206},")\n",{"type":26,"tag":147,"props":208,"children":210},{"class":149,"line":209},3,[211,216,221,225,230],{"type":26,"tag":147,"props":212,"children":213},{"style":181},[214],{"type":32,"value":215},"  .",{"type":26,"tag":147,"props":217,"children":218},{"style":187},[219],{"type":32,"value":220},"on",{"type":26,"tag":147,"props":222,"children":223},{"style":181},[224],{"type":32,"value":195},{"type":26,"tag":147,"props":226,"children":227},{"style":198},[228],{"type":32,"value":229},"'postgres_changes'",{"type":26,"tag":147,"props":231,"children":232},{"style":181},[233],{"type":32,"value":234},", {\n",{"type":26,"tag":147,"props":236,"children":238},{"class":149,"line":237},4,[239,244,249],{"type":26,"tag":147,"props":240,"children":241},{"style":181},[242],{"type":32,"value":243},"    event: ",{"type":26,"tag":147,"props":245,"children":246},{"style":198},[247],{"type":32,"value":248},"'*'",{"type":26,"tag":147,"props":250,"children":251},{"style":181},[252],{"type":32,"value":253},",\n",{"type":26,"tag":147,"props":255,"children":257},{"class":149,"line":256},5,[258,263,268],{"type":26,"tag":147,"props":259,"children":260},{"style":181},[261],{"type":32,"value":262},"    schema: ",{"type":26,"tag":147,"props":264,"children":265},{"style":198},[266],{"type":32,"value":267},"'public'",{"type":26,"tag":147,"props":269,"children":270},{"style":181},[271],{"type":32,"value":253},{"type":26,"tag":147,"props":273,"children":275},{"class":149,"line":274},6,[276,281,285],{"type":26,"tag":147,"props":277,"children":278},{"style":181},[279],{"type":32,"value":280},"    table: ",{"type":26,"tag":147,"props":282,"children":283},{"style":198},[284],{"type":32,"value":201},{"type":26,"tag":147,"props":286,"children":287},{"style":181},[288],{"type":32,"value":253},{"type":26,"tag":147,"props":290,"children":292},{"class":149,"line":291},7,[293,298,303,308,313],{"type":26,"tag":147,"props":294,"children":295},{"style":181},[296],{"type":32,"value":297},"    filter: ",{"type":26,"tag":147,"props":299,"children":300},{"style":198},[301],{"type":32,"value":302},"`clinic_id=eq.${",{"type":26,"tag":147,"props":304,"children":305},{"style":181},[306],{"type":32,"value":307},"clinicId",{"type":26,"tag":147,"props":309,"children":310},{"style":198},[311],{"type":32,"value":312},"}`",{"type":26,"tag":147,"props":314,"children":315},{"style":181},[316],{"type":32,"value":253},{"type":26,"tag":147,"props":318,"children":320},{"class":149,"line":319},8,[321,326,332,337,342],{"type":26,"tag":147,"props":322,"children":323},{"style":181},[324],{"type":32,"value":325},"  }, (",{"type":26,"tag":147,"props":327,"children":329},{"style":328},"--shiki-default:#E36209;--shiki-dark:#FFAB70",[330],{"type":32,"value":331},"payload",{"type":26,"tag":147,"props":333,"children":334},{"style":181},[335],{"type":32,"value":336},") ",{"type":26,"tag":147,"props":338,"children":339},{"style":164},[340],{"type":32,"value":341},"=>",{"type":26,"tag":147,"props":343,"children":344},{"style":181},[345],{"type":32,"value":346}," {\n",{"type":26,"tag":147,"props":348,"children":350},{"class":149,"line":349},9,[351,356,361],{"type":26,"tag":147,"props":352,"children":353},{"style":181},[354],{"type":32,"value":355},"    appointmentStore.",{"type":26,"tag":147,"props":357,"children":358},{"style":187},[359],{"type":32,"value":360},"handleRealtimeEvent",{"type":26,"tag":147,"props":362,"children":363},{"style":181},[364],{"type":32,"value":365},"(payload)\n",{"type":26,"tag":147,"props":367,"children":369},{"class":149,"line":368},10,[370],{"type":26,"tag":147,"props":371,"children":372},{"style":181},[373],{"type":32,"value":374},"  })\n",{"type":26,"tag":147,"props":376,"children":378},{"class":149,"line":377},11,[379,383,388],{"type":26,"tag":147,"props":380,"children":381},{"style":181},[382],{"type":32,"value":215},{"type":26,"tag":147,"props":384,"children":385},{"style":187},[386],{"type":32,"value":387},"subscribe",{"type":26,"tag":147,"props":389,"children":390},{"style":181},[391],{"type":32,"value":392},"()\n",{"type":26,"tag":27,"props":394,"children":396},{"id":395},"ผลลัพธ์",[397],{"type":32,"value":395},{"type":26,"tag":58,"props":399,"children":400},{},[401,413,425,437],{"type":26,"tag":62,"props":402,"children":403},{},[404,406,411],{"type":32,"value":405},"เวลาที่ใช้ในการจัดตารางนัดลดจาก ",{"type":26,"tag":50,"props":407,"children":408},{},[409],{"type":32,"value":410},"12 นาที → 2 นาที",{"type":32,"value":412}," ต่อผู้ป่วย",{"type":26,"tag":62,"props":414,"children":415},{},[416,418,423],{"type":32,"value":417},"Error rate ในการออกใบเสร็จลดลง ",{"type":26,"tag":50,"props":419,"children":420},{},[421],{"type":32,"value":422},"95%",{"type":32,"value":424}," (จาก manual entry)",{"type":26,"tag":62,"props":426,"children":427},{},[428,430,435],{"type":32,"value":429},"Staff satisfaction score ",{"type":26,"tag":50,"props":431,"children":432},{},[433],{"type":32,"value":434},"4.6\u002F5",{"type":32,"value":436}," (จาก 8 คลินิก pilot)",{"type":26,"tag":62,"props":438,"children":439},{},[440,442,447,449],{"type":32,"value":441},"Lighthouse score: Performance ",{"type":26,"tag":50,"props":443,"children":444},{},[445],{"type":32,"value":446},"94",{"type":32,"value":448},", Accessibility ",{"type":26,"tag":50,"props":450,"children":451},{},[452],{"type":32,"value":453},"98",{"type":26,"tag":455,"props":456,"children":457},"blockquote",{},[458,463],{"type":26,"tag":34,"props":459,"children":460},{},[461],{"type":32,"value":462},"\"ก่อนหน้านี้ reception ต้องเปิด Excel หลายไฟล์พร้อมกัน ตอนนี้ทุกอย่างอยู่ในหน้าเดียว\"",{"type":26,"tag":34,"props":464,"children":465},{},[466],{"type":32,"value":467},"— คุณพรรณี สุขสวัสดิ์, ผู้จัดการคลินิก",{"type":26,"tag":469,"props":470,"children":471},"style",{},[472],{"type":32,"value":473},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":7,"searchDepth":160,"depth":160,"links":475},[476,477,478,479],{"id":29,"depth":160,"text":29},{"id":41,"depth":160,"text":41},{"id":107,"depth":160,"text":110},{"id":395,"depth":160,"text":395},"markdown","content:portfolio:medsync-dashboard.md","content","portfolio\u002Fmedsync-dashboard.md","portfolio\u002Fmedsync-dashboard","md",[487,491,492,496,500,503],{"_path":488,"title":489,"category":490},"\u002Fportfolio\u002Ffreshmarket-platform","FreshMarket — แพลตฟอร์มตลาดสดออนไลน์","E-commerce",{"_path":4,"title":8,"category":9},{"_path":493,"title":494,"category":495},"\u002Fportfolio\u002Frunclub-app","RunClub — แอปสำหรับชมรมวิ่งและ community นักวิ่งไทย","Mobile",{"_path":497,"title":498,"category":499},"\u002Fportfolio\u002Fbaan-design-studio","Baan Design Studio — Portfolio Website สำหรับสตูดิโอออกแบบ","Web",{"_path":501,"title":502,"category":9},"\u002Fportfolio\u002Forchard-saas","Orchard — HR & Payroll SaaS สำหรับ SME ไทย",{"_path":504,"title":505,"category":495},"\u002Fportfolio\u002Fbitebuddy-food-app","BiteBuddy — แอปค้นหาร้านอาหารและรีวิวเพื่อชุมชน",1779878303272]