Skip to content

Commit 1b7010a

Browse files
authored
feat: add inline editing for surgery view page (#18)
Transform the surgery view page into an editable document where staff can quickly update any field before printing. Key changes: - Add InlineEditableField base component with keyboard shortcuts - Add InlineEditableRichText for rich text fields (notes, post-op, etc.) - Add InlineEditableText for simple text fields (BHT, ward) - Add InlineEditableDate for date picker fields - Add InlineEditableDoctors for doctor multi-select with search - Add EditableFieldCard wrapper component - All fields are now always visible with click-to-add placeholders - Each field saves independently with toast notifications - Rename "Edit" button to "Edit All" for bulk editing - Display notes cards as full-width rows for easier editing
1 parent 2c3f0f3 commit 1b7010a

8 files changed

Lines changed: 1069 additions & 171 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { cn } from '@renderer/lib/utils'
2+
import { Card, CardContent, CardHeader, CardTitle } from '@renderer/components/ui/card'
3+
import { LucideIcon } from 'lucide-react'
4+
import { ReactNode } from 'react'
5+
6+
export interface EditableFieldCardProps {
7+
/** Card title */
8+
title: string
9+
/** Icon component */
10+
icon: LucideIcon
11+
/** Icon background color class */
12+
iconBgColor: string
13+
/** Icon color class */
14+
iconColor: string
15+
/** Card content */
16+
children: ReactNode
17+
/** Additional class name for the card */
18+
className?: string
19+
/** Animation delay for entrance animation */
20+
animationDelay?: number
21+
}
22+
23+
export const EditableFieldCard = ({
24+
title,
25+
icon: Icon,
26+
iconBgColor,
27+
iconColor,
28+
children,
29+
className,
30+
animationDelay = 0
31+
}: EditableFieldCardProps) => {
32+
return (
33+
<Card
34+
className={cn('bg-gradient-to-br from-card to-card/80 animate-fade-in-up', className)}
35+
style={{ animationDelay: `${animationDelay}ms` }}
36+
>
37+
<CardHeader className="pb-3 pt-4">
38+
<div className="flex items-center gap-2.5">
39+
<div className={cn('h-8 w-8 rounded-lg flex items-center justify-center', iconBgColor)}>
40+
<Icon className={cn('h-4 w-4', iconColor)} />
41+
</div>
42+
<CardTitle className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
43+
{title}
44+
</CardTitle>
45+
</div>
46+
</CardHeader>
47+
<CardContent className="pt-0">{children}</CardContent>
48+
</Card>
49+
)
50+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { cn, formatDate } from '@renderer/lib/utils'
2+
import { Button } from '@renderer/components/ui/button'
3+
import { Calendar } from '@renderer/components/ui/calendar'
4+
import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover'
5+
import { Pencil, X } from 'lucide-react'
6+
import { useState, useCallback } from 'react'
7+
8+
export interface InlineEditableDateProps {
9+
/** Current date value */
10+
value: Date | null | undefined
11+
/** Called when date is saved */
12+
onSave: (value: Date | null) => Promise<void>
13+
/** Placeholder for empty state */
14+
emptyPlaceholder?: string
15+
/** Additional class name */
16+
className?: string
17+
}
18+
19+
export const InlineEditableDate = ({
20+
value,
21+
onSave,
22+
emptyPlaceholder = 'Select date...',
23+
className
24+
}: InlineEditableDateProps) => {
25+
const [isOpen, setIsOpen] = useState(false)
26+
const [isSaving, setIsSaving] = useState(false)
27+
28+
const isEmpty = !value
29+
30+
const handleSelect = useCallback(
31+
async (date: Date | undefined) => {
32+
setIsSaving(true)
33+
try {
34+
await onSave(date || null)
35+
setIsOpen(false)
36+
} finally {
37+
setIsSaving(false)
38+
}
39+
},
40+
[onSave]
41+
)
42+
43+
const handleClear = useCallback(
44+
async (e: React.MouseEvent) => {
45+
e.stopPropagation()
46+
setIsSaving(true)
47+
try {
48+
await onSave(null)
49+
setIsOpen(false)
50+
} finally {
51+
setIsSaving(false)
52+
}
53+
},
54+
[onSave]
55+
)
56+
57+
return (
58+
<Popover open={isOpen} onOpenChange={setIsOpen}>
59+
<PopoverTrigger asChild>
60+
<div
61+
className={cn(
62+
'group inline-flex items-center gap-2 cursor-pointer transition-all duration-200 rounded px-1 -mx-1',
63+
isEmpty ? 'text-muted-foreground/60' : '',
64+
'hover:bg-accent/50',
65+
className
66+
)}
67+
role="button"
68+
tabIndex={0}
69+
>
70+
<span className="text-sm font-medium">
71+
{isEmpty ? emptyPlaceholder : formatDate(value)}
72+
</span>
73+
<Pencil className="h-3 w-3 text-muted-foreground/40 opacity-0 group-hover:opacity-100 transition-opacity" />
74+
</div>
75+
</PopoverTrigger>
76+
<PopoverContent className="w-auto p-0" align="start">
77+
<Calendar
78+
mode="single"
79+
selected={value || undefined}
80+
onSelect={handleSelect}
81+
initialFocus
82+
disabled={isSaving}
83+
/>
84+
{value && (
85+
<div className="px-3 pb-3 border-t pt-3">
86+
<Button
87+
variant="ghost"
88+
size="sm"
89+
className="w-full text-muted-foreground"
90+
onClick={handleClear}
91+
disabled={isSaving}
92+
>
93+
<X className="h-4 w-4 mr-2" />
94+
Clear date
95+
</Button>
96+
</div>
97+
)}
98+
</PopoverContent>
99+
</Popover>
100+
)
101+
}

0 commit comments

Comments
 (0)