mirror of
https://github.com/Tony0410/nextstep.git
synced 2026-05-25 05:41:39 +08:00
Fix medication scheduling bugs and add delete dose feature
This commit is contained in:
@@ -41,7 +41,7 @@ RUN addgroup --system --gid 1001 nodejs
|
|||||||
RUN adduser --system --uid 1001 nextjs
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
# Install OpenSSL and CA certificates for Prisma
|
# Install OpenSSL and CA certificates for Prisma
|
||||||
RUN apt-get update && apt-get install -y openssl ca-certificates && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y openssl ca-certificates wget && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY --from=builder /app/public ./public
|
COPY --from=builder /app/public ./public
|
||||||
COPY --from=builder /app/prisma ./prisma
|
COPY --from=builder /app/prisma ./prisma
|
||||||
|
|||||||
@@ -1,19 +1,41 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { use, useEffect, useState, useCallback } from 'react'
|
import { useEffect, useState, useCallback } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
import { Pill, Clock, Edit2, Trash2, History } from 'lucide-react'
|
import { Pill, Clock, Trash2, History, X } from 'lucide-react'
|
||||||
import { useLiveQuery } from 'dexie-react-hooks'
|
import { useLiveQuery } from 'dexie-react-hooks'
|
||||||
|
|
||||||
import { db, logDose, undoDose } from '@/lib/sync'
|
import { db, logDose, undoDose } from '@/lib/sync'
|
||||||
|
import type { LocalDoseLog } from '@/lib/sync'
|
||||||
import { Card, Button, LoadingState, Modal, showToast, showUndoToast } from '@/components/ui'
|
import { Card, Button, LoadingState, Modal, showToast, showUndoToast } from '@/components/ui'
|
||||||
import { Header, PageContainer } from '@/components/layout/header'
|
import { Header, PageContainer } from '@/components/layout/header'
|
||||||
import { RefillTracker } from '@/components/medications/RefillTracker'
|
import { RefillTracker } from '@/components/medications/RefillTracker'
|
||||||
import { useApp } from '../../provider'
|
import { useApp } from '../../provider'
|
||||||
|
|
||||||
export default function MedicationDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
// Unwrapping params for Next.js 14/15 compatibility
|
||||||
const { id: medicationId } = use(params)
|
// In Next.js 15 params is a Promise, in 14 it's an object.
|
||||||
|
// We can use a simple `use` polyfill or just await it if we were in an async component,
|
||||||
|
// but this is a client component.
|
||||||
|
// For client components, params is passed as is.
|
||||||
|
// If types say Promise, we might need to use `use` but `use` is experimental in React 18.
|
||||||
|
// Let's assume params is an object for now as per Next 14 standard behavior for pages.
|
||||||
|
// If it is a promise (Next 15), we need `use`.
|
||||||
|
// Safest way: check if it has .then?
|
||||||
|
// Actually, let's just assume object for Next 14.
|
||||||
|
|
||||||
|
export default function MedicationDetailPage({ params }: { params: { id: string } | Promise<{ id: string }> }) {
|
||||||
|
// Simple unwrap if it's a promise (though likely it's an object in Next 14)
|
||||||
|
const [medicationId, setMedicationId] = useState<string>('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (params instanceof Promise) {
|
||||||
|
params.then((p) => setMedicationId(p.id))
|
||||||
|
} else {
|
||||||
|
setMedicationId(params.id)
|
||||||
|
}
|
||||||
|
}, [params])
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { currentWorkspace, refreshData } = useApp()
|
const { currentWorkspace, refreshData } = useApp()
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||||
@@ -21,19 +43,21 @@ export default function MedicationDetailPage({ params }: { params: Promise<{ id:
|
|||||||
|
|
||||||
// Fetch medication from IndexedDB
|
// Fetch medication from IndexedDB
|
||||||
const medication = useLiveQuery(
|
const medication = useLiveQuery(
|
||||||
() => db.medications.get(medicationId),
|
() => (medicationId ? db.medications.get(medicationId) : undefined),
|
||||||
[medicationId]
|
[medicationId]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Fetch recent dose logs
|
// Fetch recent dose logs
|
||||||
const doseLogs = useLiveQuery(
|
const doseLogs = useLiveQuery(
|
||||||
() =>
|
() =>
|
||||||
db.doseLogs
|
medicationId
|
||||||
|
? db.doseLogs
|
||||||
.where('medicationId')
|
.where('medicationId')
|
||||||
.equals(medicationId)
|
.equals(medicationId)
|
||||||
.reverse()
|
.reverse()
|
||||||
.limit(10)
|
.limit(10)
|
||||||
.toArray(),
|
.toArray()
|
||||||
|
: [],
|
||||||
[medicationId]
|
[medicationId]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -58,6 +82,15 @@ export default function MedicationDetailPage({ params }: { params: Promise<{ id:
|
|||||||
}
|
}
|
||||||
}, [medication, currentWorkspace.id])
|
}, [medication, currentWorkspace.id])
|
||||||
|
|
||||||
|
const handleDeleteDose = async (dose: LocalDoseLog) => {
|
||||||
|
try {
|
||||||
|
await undoDose(dose)
|
||||||
|
showToast('Dose removed', 'success')
|
||||||
|
} catch {
|
||||||
|
showToast('Failed to remove dose', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!medication) return
|
if (!medication) return
|
||||||
setDeleting(true)
|
setDeleting(true)
|
||||||
@@ -79,25 +112,27 @@ export default function MedicationDetailPage({ params }: { params: Promise<{ id:
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formatSchedule = () => {
|
const formatSchedule = () => {
|
||||||
if (!medication) return ''
|
if (!medication || !medication.scheduleData) return ''
|
||||||
const data = medication.scheduleData as Record<string, unknown>
|
const data = medication.scheduleData as Record<string, unknown>
|
||||||
switch (medication.scheduleType) {
|
switch (medication.scheduleType) {
|
||||||
case 'FIXED_TIMES':
|
case 'FIXED_TIMES':
|
||||||
return `Daily at ${(data.times as string[]).join(', ')}`
|
return `Daily at ${(Array.isArray(data.times) ? data.times : []).join(', ')}`
|
||||||
case 'INTERVAL':
|
case 'INTERVAL':
|
||||||
return `Every ${data.hours} hours (starting ${data.startTime})`
|
return `Every ${data.hours || '?'} hours (starting ${data.startTime || '?'})`
|
||||||
case 'WEEKDAYS':
|
case 'WEEKDAYS':
|
||||||
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
||||||
const selectedDays = (data.days as number[]).map(d => days[d]).join(', ')
|
const selectedDays = (Array.isArray(data.days) ? data.days : [])
|
||||||
return `${selectedDays} at ${data.time}`
|
.map((d: number) => days[d])
|
||||||
|
.join(', ')
|
||||||
|
return `${selectedDays} at ${data.time || '?'}`
|
||||||
case 'PRN':
|
case 'PRN':
|
||||||
return `As needed (min ${data.minHoursBetween}h between doses)`
|
return `As needed (min ${data.minHoursBetween || '?'}h between doses)`
|
||||||
default:
|
default:
|
||||||
return medication.scheduleType
|
return medication.scheduleType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!medication) {
|
if (!medicationId || !medication) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header title="Medication" showBack />
|
<Header title="Medication" showBack />
|
||||||
@@ -193,7 +228,7 @@ export default function MedicationDetailPage({ params }: { params: Promise<{ id:
|
|||||||
<Card padding="none">
|
<Card padding="none">
|
||||||
<ul className="divide-y divide-border">
|
<ul className="divide-y divide-border">
|
||||||
{recentDoses.map((dose) => (
|
{recentDoses.map((dose) => (
|
||||||
<li key={dose.id} className="px-4 py-3 flex items-center justify-between">
|
<li key={dose.id} className="px-4 py-3 flex items-center justify-between group">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-secondary-900">
|
<p className="text-sm font-medium text-secondary-900">
|
||||||
{format(new Date(dose.takenAt), 'EEEE, MMM d')}
|
{format(new Date(dose.takenAt), 'EEEE, MMM d')}
|
||||||
@@ -203,6 +238,15 @@ export default function MedicationDetailPage({ params }: { params: Promise<{ id:
|
|||||||
{dose.loggedBy && ` by ${dose.loggedBy.name}`}
|
{dose.loggedBy && ` by ${dose.loggedBy.name}`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{currentWorkspace.role !== 'VIEWER' && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteDose(dose)}
|
||||||
|
className="p-2 text-secondary-400 hover:text-red-600 hover:bg-red-50 rounded-full transition-colors opacity-0 group-hover:opacity-100 focus:opacity-100"
|
||||||
|
title="Remove dose"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -133,6 +133,11 @@ function calculateIntervalDue(
|
|||||||
// Calculate how many intervals have passed since start time today
|
// Calculate how many intervals have passed since start time today
|
||||||
const minutesSinceStart = differenceInMinutes(now, startToday)
|
const minutesSinceStart = differenceInMinutes(now, startToday)
|
||||||
const intervalMinutes = schedule.hours * 60
|
const intervalMinutes = schedule.hours * 60
|
||||||
|
|
||||||
|
if (intervalMinutes <= 0) {
|
||||||
|
return startToday
|
||||||
|
}
|
||||||
|
|
||||||
const intervalsPassed = Math.floor(minutesSinceStart / intervalMinutes)
|
const intervalsPassed = Math.floor(minutesSinceStart / intervalMinutes)
|
||||||
const nextDue = addMinutes(startToday, (intervalsPassed + 1) * intervalMinutes)
|
const nextDue = addMinutes(startToday, (intervalsPassed + 1) * intervalMinutes)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user