feat: Add Medication Refill feature with mobile UI improvements (#30)
* feat: Add Medication Refill feature with UI improvements - Add refill functionality to medications (add packs/loose pills) - Add refill API endpoint with history tracking - Add refill section in edit forms (desktop & mobile) - Add refill modal in medication detail view - Add refill history display with expand/collapse - Add schedule lightbox for clicking medication images - Improve button styling with primary/info/success classes - Move '+ New entry' button to medication list header - Lightbox size: 50% desktop, 90% mobile - Update selectedMed sync after stock changes - Migrate from schema-sql.ts to Drizzle Kit migrations * fix: Improve mobile tooltips and refill modal layout - Center tooltips on screen for mobile devices (fixed position) - Close tooltips automatically when scrolling on touch devices - Use click-based tooltip activation instead of hover on mobile - Fix refill modal buttons to display in two rows on mobile
This commit is contained in:
+370
-30
@@ -39,6 +39,13 @@ type PlannerRow = {
|
||||
enough: boolean;
|
||||
};
|
||||
|
||||
type RefillEntry = {
|
||||
id: number;
|
||||
packsAdded: number;
|
||||
loosePillsAdded: number;
|
||||
refillDate: string;
|
||||
};
|
||||
|
||||
type FormBlister = { usage: string; every: string; startDate: string; startTime: string };
|
||||
|
||||
type FormState = {
|
||||
@@ -337,6 +344,7 @@ function AppContent() {
|
||||
const [pendingImagePreview, setPendingImagePreview] = useState<string | null>(null);
|
||||
const [selectedMed, setSelectedMed] = useState<Medication | null>(null);
|
||||
const [showImageLightbox, setShowImageLightbox] = useState(false);
|
||||
const [scheduleLightboxImage, setScheduleLightboxImage] = useState<string | null>(null);
|
||||
const [selectedUser, setSelectedUser] = useState<string | null>(null);
|
||||
const [scheduleDays, setScheduleDays] = useState<number>(30);
|
||||
const [showPastDays, setShowPastDays] = useState(false);
|
||||
@@ -358,9 +366,18 @@ function AppContent() {
|
||||
// Export/Import state
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [importing, setImporting] = useState(false);
|
||||
// User dropdown state (for mobile click-based behavior)
|
||||
const [userDropdownOpen, setUserDropdownOpen] = useState(false);
|
||||
|
||||
const [showImportConfirm, setShowImportConfirm] = useState(false);
|
||||
const [pendingImportData, setPendingImportData] = useState<any>(null);
|
||||
// Refill state
|
||||
const [showRefillModal, setShowRefillModal] = useState(false);
|
||||
const [refillPacks, setRefillPacks] = useState(1);
|
||||
const [refillLoose, setRefillLoose] = useState(0);
|
||||
const [refillSaving, setRefillSaving] = useState(false);
|
||||
const [refillHistory, setRefillHistory] = useState<RefillEntry[]>([]);
|
||||
const [refillHistoryExpanded, setRefillHistoryExpanded] = useState(false);
|
||||
// Collapsed days state (manually collapsed days are persisted)
|
||||
const [manuallyCollapsedDays, setManuallyCollapsedDays] = useState<Set<string>>(new Set());
|
||||
const [manuallyExpandedDays, setManuallyExpandedDays] = useState<Set<string>>(new Set());
|
||||
@@ -515,7 +532,11 @@ function AppContent() {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
// Close modals in order of priority (topmost first)
|
||||
if (showImageLightbox) {
|
||||
if (userDropdownOpen) {
|
||||
setUserDropdownOpen(false);
|
||||
} else if (scheduleLightboxImage) {
|
||||
setScheduleLightboxImage(null);
|
||||
} else if (showImageLightbox) {
|
||||
setShowImageLightbox(false);
|
||||
} else if (showEditModal) {
|
||||
setShowEditModal(false);
|
||||
@@ -533,7 +554,54 @@ function AppContent() {
|
||||
};
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
return () => document.removeEventListener("keydown", handleEscape);
|
||||
}, [selectedMed, showImageLightbox, selectedUser, showProfile, showShareDialog, showEditModal]);
|
||||
}, [selectedMed, showImageLightbox, scheduleLightboxImage, selectedUser, showProfile, showShareDialog, showEditModal, userDropdownOpen]);
|
||||
|
||||
// Close user dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
if (!userDropdownOpen) return;
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target.closest('.user-menu')) {
|
||||
setUserDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("click", handleClickOutside);
|
||||
return () => document.removeEventListener("click", handleClickOutside);
|
||||
}, [userDropdownOpen]);
|
||||
|
||||
// Close tooltips on scroll/touch (for mobile)
|
||||
useEffect(() => {
|
||||
const closeAllTooltips = () => {
|
||||
document.querySelectorAll('.info-tooltip.tooltip-active').forEach(el => {
|
||||
el.classList.remove('tooltip-active');
|
||||
});
|
||||
};
|
||||
|
||||
const handleTooltipClick = (e: Event) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.classList.contains('info-tooltip')) {
|
||||
// Close other tooltips first
|
||||
closeAllTooltips();
|
||||
// Toggle this one
|
||||
target.classList.add('tooltip-active');
|
||||
} else {
|
||||
closeAllTooltips();
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchMove = () => {
|
||||
closeAllTooltips();
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleTooltipClick, { capture: true });
|
||||
document.addEventListener('touchmove', handleTouchMove, { passive: true });
|
||||
document.addEventListener('scroll', handleTouchMove, { passive: true });
|
||||
return () => {
|
||||
document.removeEventListener('click', handleTooltipClick, { capture: true });
|
||||
document.removeEventListener('touchmove', handleTouchMove);
|
||||
document.removeEventListener('scroll', handleTouchMove);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Prevent background scroll when modal is open
|
||||
useEffect(() => {
|
||||
@@ -556,6 +624,20 @@ function AppContent() {
|
||||
};
|
||||
}, [selectedMed, selectedUser, showProfile, showShareDialog, showEditModal]);
|
||||
|
||||
// Update selectedMed when meds change (e.g., after refill)
|
||||
useEffect(() => {
|
||||
if (selectedMed) {
|
||||
const updated = meds.find(m => m.id === selectedMed.id);
|
||||
if (updated && (
|
||||
updated.packCount !== selectedMed.packCount ||
|
||||
updated.looseTablets !== selectedMed.looseTablets ||
|
||||
updated.updatedAt !== selectedMed.updatedAt
|
||||
)) {
|
||||
setSelectedMed(updated);
|
||||
}
|
||||
}
|
||||
}, [meds, selectedMed]);
|
||||
|
||||
// Check if settings have changed
|
||||
const settingsChanged = settings.emailEnabled !== savedSettings.emailEnabled ||
|
||||
settings.notificationEmail !== savedSettings.notificationEmail ||
|
||||
@@ -739,6 +821,65 @@ function AppContent() {
|
||||
setSettingsSaved(true);
|
||||
}
|
||||
|
||||
// Load refill history for a medication
|
||||
async function loadRefillHistory(medId: number) {
|
||||
try {
|
||||
const res = await fetch(`/api/medications/${medId}/refills`, { credentials: "include" });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setRefillHistory(Array.isArray(data) ? data : (data.refills || []));
|
||||
} else {
|
||||
setRefillHistory([]);
|
||||
}
|
||||
} catch {
|
||||
setRefillHistory([]);
|
||||
}
|
||||
}
|
||||
|
||||
// Submit a refill
|
||||
async function submitRefill(medId: number) {
|
||||
if (refillPacks < 1 && refillLoose < 1) return;
|
||||
setRefillSaving(true);
|
||||
try {
|
||||
const res = await fetch(`/api/medications/${medId}/refill`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ packsAdded: refillPacks, loosePillsAdded: refillLoose }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
// Update form values if we're in edit mode
|
||||
if (editingId === medId && data.newStock) {
|
||||
setForm(f => ({
|
||||
...f,
|
||||
packCount: String(data.newStock.packCount),
|
||||
looseTablets: String(data.newStock.looseTablets),
|
||||
}));
|
||||
}
|
||||
// Reset refill form
|
||||
setRefillPacks(1);
|
||||
setRefillLoose(0);
|
||||
setShowRefillModal(false);
|
||||
// Reload medications to get updated stock
|
||||
loadMeds();
|
||||
// Reload refill history
|
||||
await loadRefillHistory(medId);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
setRefillSaving(false);
|
||||
}
|
||||
|
||||
// Helper to open medication detail modal with refill history
|
||||
function openMedDetail(med: Medication) {
|
||||
setSelectedMed(med);
|
||||
setRefillHistory([]);
|
||||
setRefillHistoryExpanded(false);
|
||||
loadRefillHistory(med.id);
|
||||
}
|
||||
|
||||
async function testEmail() {
|
||||
if (!settings.notificationEmail) return;
|
||||
setTestingEmail(true);
|
||||
@@ -1286,8 +1427,8 @@ function AppContent() {
|
||||
{theme === "dark" ? "☀️" : "🌙"}
|
||||
</button>
|
||||
{authState?.authEnabled && user && (
|
||||
<div className="user-menu">
|
||||
<button className="user-menu-btn">
|
||||
<div className={`user-menu ${userDropdownOpen ? 'open' : ''}`}>
|
||||
<button className="user-menu-btn" onClick={() => setUserDropdownOpen(!userDropdownOpen)}>
|
||||
{user.avatarUrl ? (
|
||||
<img src={`/api/images/${user.avatarUrl}`} alt={user.username} className="user-avatar-img" />
|
||||
) : (
|
||||
@@ -1304,15 +1445,15 @@ function AppContent() {
|
||||
<span className="dropdown-username">{user.username}</span>
|
||||
</div>
|
||||
<div className="dropdown-menu">
|
||||
<button className="dropdown-item" onClick={() => setShowProfile(true)}>
|
||||
<button className="dropdown-item" onClick={() => { setShowProfile(true); setUserDropdownOpen(false); }}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
{t('auth.profile', 'Profile')}
|
||||
</button>
|
||||
<button className="dropdown-item" onClick={() => navigate('/settings')}>
|
||||
<button className="dropdown-item" onClick={() => { navigate('/settings'); setUserDropdownOpen(false); }}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
||||
{t('nav.settings', 'Settings')}
|
||||
</button>
|
||||
<button className="dropdown-item danger" onClick={() => logout()}>
|
||||
<button className="dropdown-item danger" onClick={() => { logout(); setUserDropdownOpen(false); }}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
||||
{t('auth.signOut', 'Sign Out')}
|
||||
</button>
|
||||
@@ -1398,7 +1539,7 @@ function AppContent() {
|
||||
med ? med.packCount * med.blistersPerPack * med.pillsPerBlister + med.looseTablets : Math.round(row.medsLeft)
|
||||
);
|
||||
return (
|
||||
<div key={row.name} className="table-row clickable" onClick={() => med && setSelectedMed(med)}>
|
||||
<div key={row.name} className="table-row clickable" onClick={() => med && openMedDetail(med)}>
|
||||
<span data-label={t('table.name')} className="cell-with-avatar">
|
||||
<MedicationAvatar name={row.name} imageUrl={med?.imageUrl} />
|
||||
<span className="med-name-text">{row.name}</span>
|
||||
@@ -1467,7 +1608,7 @@ function AppContent() {
|
||||
med ? med.packCount * med.blistersPerPack * med.pillsPerBlister + med.looseTablets : Math.round(row.medsLeft)
|
||||
);
|
||||
return (
|
||||
<div key={row.name} className="table-row clickable" onClick={() => med && setSelectedMed(med)}>
|
||||
<div key={row.name} className="table-row clickable" onClick={() => med && openMedDetail(med)}>
|
||||
<span data-label={t('table.name')} className="cell-with-avatar">
|
||||
<span className="med-name-line">
|
||||
<MedicationAvatar name={row.name} imageUrl={med?.imageUrl} />
|
||||
@@ -1595,7 +1736,15 @@ function AppContent() {
|
||||
return (
|
||||
<div key={`${day.dateStr}-${item.medName}`} className={`time-row ${allTaken ? "taken" : ""}`}>
|
||||
<div className="time-main">
|
||||
<div className="med-name"><MedicationAvatar name={item.medName} imageUrl={med?.imageUrl} size="sm" /><span className="med-name-text">{item.medName}</span>{med?.intakeRemindersEnabled && <span className="reminder-icon info-tooltip" data-tooltip={t('tooltips.intakeReminders')}>🔔</span>}</div>
|
||||
<div className="med-name">
|
||||
<div
|
||||
className={med?.imageUrl ? "med-avatar clickable" : ""}
|
||||
onClick={() => med?.imageUrl && setScheduleLightboxImage(`/api/images/${med.imageUrl}`)}
|
||||
>
|
||||
<MedicationAvatar name={item.medName} imageUrl={med?.imageUrl} size="sm" />
|
||||
</div>
|
||||
<span className="med-name-text">{item.medName}</span>{med?.intakeRemindersEnabled && <span className="reminder-icon info-tooltip" data-tooltip={t('tooltips.intakeReminders')}>🔔</span>}
|
||||
</div>
|
||||
<div className="tag-row">
|
||||
<span className="tag subtle">{item.total} {t('common.pills')} {t('common.total')}</span>
|
||||
</div>
|
||||
@@ -1698,7 +1847,15 @@ function AppContent() {
|
||||
return (
|
||||
<div key={`${day.dateStr}-${item.medName}`} className={`time-row ${allTaken ? "taken" : ""}`}>
|
||||
<div className="time-main">
|
||||
<div className="med-name"><MedicationAvatar name={item.medName} imageUrl={med?.imageUrl} size="sm" /><span className="med-name-text">{item.medName}</span>{med?.intakeRemindersEnabled && <span className="reminder-icon info-tooltip" data-tooltip={t('tooltips.intakeReminders')}>🔔</span>}</div>
|
||||
<div className="med-name">
|
||||
<div
|
||||
className={med?.imageUrl ? "med-avatar clickable" : ""}
|
||||
onClick={() => med?.imageUrl && setScheduleLightboxImage(`/api/images/${med.imageUrl}`)}
|
||||
>
|
||||
<MedicationAvatar name={item.medName} imageUrl={med?.imageUrl} size="sm" />
|
||||
</div>
|
||||
<span className="med-name-text">{item.medName}</span>{med?.intakeRemindersEnabled && <span className="reminder-icon info-tooltip" data-tooltip={t('tooltips.intakeReminders')}>🔔</span>}
|
||||
</div>
|
||||
<div className="tag-row">
|
||||
<span className="tag subtle">{item.total} {t('common.pills')} {t('common.total')}</span>
|
||||
{status && <span className={`tag ${status.className}`}>
|
||||
@@ -1788,6 +1945,19 @@ function AppContent() {
|
||||
<article className="card meds">
|
||||
<div className="card-head">
|
||||
<h2>{t('medications.list.title')}</h2>
|
||||
<button
|
||||
type="button"
|
||||
className="btn primary small"
|
||||
onClick={() => {
|
||||
resetForm();
|
||||
// On mobile, open the edit modal
|
||||
if (window.innerWidth <= 768) {
|
||||
setShowEditModal(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
+ {t('form.newEntry')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="med-list">
|
||||
{meds.map((med) => (
|
||||
@@ -1807,7 +1977,7 @@ function AppContent() {
|
||||
<div className="med-total">{t('medications.details.total')}: {med.packCount * med.blistersPerPack * med.pillsPerBlister + med.looseTablets} {t('common.pills')}</div>
|
||||
</div>
|
||||
<div className="med-actions">
|
||||
<button className="secondary" onClick={() => startEdit(med)}>{t('common.edit')}</button>
|
||||
<button className="info" onClick={() => startEdit(med)}>{t('common.edit')}</button>
|
||||
<button className="danger" onClick={() => deleteMed(med.id)}>{t('common.delete')}</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1909,6 +2079,44 @@ function AppContent() {
|
||||
<input type="date" value={form.expiryDate} onChange={(e) => handleValueChange("expiryDate", e.target.value)} placeholder={t('common.optional')} />
|
||||
</label>
|
||||
|
||||
{/* Refill section - only shown when editing */}
|
||||
{editingId && (
|
||||
<div className="full refill-section">
|
||||
<h4 className="refill-title">{t('refill.title')}</h4>
|
||||
<div className="refill-form-inline">
|
||||
<label>
|
||||
{t('refill.packs')}
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
value={refillPacks}
|
||||
onChange={(e) => setRefillPacks(parseInt(e.target.value) || 0)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{t('refill.loosePills')}
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
value={refillLoose}
|
||||
onChange={(e) => setRefillLoose(parseInt(e.target.value) || 0)}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="success"
|
||||
onClick={() => submitRefill(editingId)}
|
||||
disabled={(refillPacks < 1 && refillLoose < 1) || refillSaving}
|
||||
>
|
||||
{refillSaving ? t('refill.adding') : t('refill.button')}
|
||||
</button>
|
||||
{(refillPacks > 0 || refillLoose > 0) && (
|
||||
<span className="refill-preview">+{refillPacks * Number(form.blistersPerPack || 0) * Number(form.pillsPerBlister || 1) + refillLoose} {t('common.pills')}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<label className={`full ${fieldErrors.notes ? 'has-error' : ''}`}>
|
||||
{t('form.notes')}
|
||||
<textarea
|
||||
@@ -1940,7 +2148,7 @@ function AppContent() {
|
||||
/>
|
||||
<span>🔔 {t('form.blisters.remind')}</span>
|
||||
</label>
|
||||
<button type="button" className="ghost" onClick={addBlister}>+ {t('form.blisters.addIntake')}</button>
|
||||
<button type="button" className="primary" onClick={addBlister}>+ {t('form.blisters.addIntake')}</button>
|
||||
</div>
|
||||
</div>
|
||||
{form.blisters.map((s, idx) => (
|
||||
@@ -2068,7 +2276,7 @@ function AppContent() {
|
||||
{plannerRows.map((row) => {
|
||||
const med = meds.find(m => m.name === row.medicationName);
|
||||
return (
|
||||
<div key={row.medicationId} className="table-row clickable" onClick={() => med && setSelectedMed(med)}>
|
||||
<div key={row.medicationId} className="table-row clickable" onClick={() => med && openMedDetail(med)}>
|
||||
<span data-label={t('planner.table.medication')} className="cell-with-avatar"><MedicationAvatar name={row.medicationName} imageUrl={med?.imageUrl} />{row.medicationName}</span>
|
||||
<span data-label={t('planner.table.usage')}><strong>{row.plannerUsage}</strong> {t('common.pills')}</span>
|
||||
<span data-label={t('planner.table.blisters')}>{row.blistersNeeded} × {row.blisterSize}</span>
|
||||
@@ -2196,7 +2404,7 @@ function AppContent() {
|
||||
)}
|
||||
|
||||
{/* Skip reminders for taken doses */}
|
||||
<div className="setting-row compact" style={{marginTop: "16px", paddingTop: "16px", borderTop: "1px solid var(--border-color)"}}>
|
||||
<div className="setting-row compact" style={{marginTop: "16px"}}>
|
||||
<label className="setting-label">
|
||||
{t('settings.notifications.skipTakenDoses')}
|
||||
<span className="info-tooltip small" data-tooltip={t('settings.notifications.skipTakenDosesTooltip')}>ⓘ</span>
|
||||
@@ -2519,11 +2727,13 @@ function AppContent() {
|
||||
</h2>
|
||||
</div>
|
||||
<div className="setting-section">
|
||||
<div className="export-import-grid">
|
||||
<div className="setting-group">
|
||||
{/* Export */}
|
||||
<div className="export-import-card">
|
||||
<h3>{t('exportImport.exportTitle')}</h3>
|
||||
<p className="export-import-desc">{t('exportImport.exportDesc')}</p>
|
||||
<div className="action-card">
|
||||
<div className="action-card-content">
|
||||
<span className="action-card-title">{t('exportImport.exportTitle')}</span>
|
||||
<span className="action-card-desc">{t('exportImport.exportDesc')}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary"
|
||||
@@ -2535,16 +2745,19 @@ function AppContent() {
|
||||
</div>
|
||||
|
||||
{/* Import */}
|
||||
<div className="export-import-card">
|
||||
<h3>{t('exportImport.importTitle')}</h3>
|
||||
<p className="export-import-desc">{t('exportImport.importDesc')}</p>
|
||||
<label className="export-import-file-btn">
|
||||
<div className="action-card">
|
||||
<div className="action-card-content">
|
||||
<span className="action-card-title">{t('exportImport.importTitle')}</span>
|
||||
<span className="action-card-desc">{t('exportImport.importDesc')}</span>
|
||||
</div>
|
||||
<label className="btn secondary">
|
||||
{importing ? t('exportImport.importing') : t('exportImport.import')}
|
||||
<input
|
||||
type="file"
|
||||
accept=".json,application/json"
|
||||
onChange={handleImportFileSelect}
|
||||
disabled={importing}
|
||||
style={{display: 'none'}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
@@ -2921,6 +3134,33 @@ function AppContent() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Refill History */}
|
||||
{refillHistory.length > 0 && (
|
||||
<div className="med-detail-section">
|
||||
<h3
|
||||
className="section-header-clickable"
|
||||
onClick={() => setRefillHistoryExpanded(!refillHistoryExpanded)}
|
||||
>
|
||||
{t('refill.history')} ({refillHistory.length})
|
||||
<span className="expand-arrow">{refillHistoryExpanded ? '▼' : '▶'}</span>
|
||||
</h3>
|
||||
{refillHistoryExpanded && (
|
||||
<div className="refill-history-list">
|
||||
{refillHistory.map((entry) => (
|
||||
<div key={entry.id} className="refill-history-item">
|
||||
<span className="refill-date">
|
||||
{new Date(entry.refillDate).toLocaleDateString(i18n.language, { day: "2-digit", month: "short", year: "numeric" })}, {new Date(entry.refillDate).toLocaleTimeString(i18n.language, { hour: "2-digit", minute: "2-digit" })}
|
||||
</span>
|
||||
<span className="refill-amount">
|
||||
+{entry.packsAdded * selectedMed.blistersPerPack * selectedMed.pillsPerBlister + entry.loosePillsAdded} {t('common.pills')}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="med-detail-footer">
|
||||
@@ -2928,7 +3168,10 @@ function AppContent() {
|
||||
{t('common.close')}
|
||||
</button>
|
||||
<div className="footer-actions">
|
||||
<button className="secondary" onClick={() => { setSelectedMed(null); setShowImageLightbox(false); navigate("/medications"); startEdit(selectedMed); }}>
|
||||
<button className="success" onClick={() => setShowRefillModal(true)}>
|
||||
{t('refill.button')}
|
||||
</button>
|
||||
<button className="info" onClick={() => { setSelectedMed(null); setShowImageLightbox(false); navigate("/medications"); startEdit(selectedMed); }}>
|
||||
{t('common.edit')}
|
||||
</button>
|
||||
{selectedMed.blisters.length > 0 && (
|
||||
@@ -2952,6 +3195,56 @@ function AppContent() {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Refill Modal */}
|
||||
{showRefillModal && (
|
||||
<div className="modal-overlay" onClick={(e) => { e.stopPropagation(); setShowRefillModal(false); }}>
|
||||
<div className="modal-content refill-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<button className="modal-close" onClick={() => setShowRefillModal(false)}>×</button>
|
||||
<h2>{t('refill.title')}</h2>
|
||||
<p className="refill-med-name">{selectedMed.name}</p>
|
||||
|
||||
<div className="refill-form">
|
||||
<label>
|
||||
{t('refill.packs')}
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
value={refillPacks}
|
||||
onChange={(e) => setRefillPacks(parseInt(e.target.value) || 0)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{t('refill.loosePills')}
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
value={refillLoose}
|
||||
onChange={(e) => setRefillLoose(parseInt(e.target.value) || 0)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
<button className="ghost" onClick={() => setShowRefillModal(false)}>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<div className="refill-footer-right">
|
||||
<button
|
||||
className="success"
|
||||
onClick={() => submitRefill(selectedMed.id)}
|
||||
disabled={(refillPacks < 1 && refillLoose < 1) || refillSaving}
|
||||
>
|
||||
{refillSaving ? t('common.saving') : t('refill.button')}
|
||||
</button>
|
||||
{(refillPacks > 0 || refillLoose > 0) && (
|
||||
<span className="refill-preview">+{refillPacks * selectedMed.blistersPerPack * selectedMed.pillsPerBlister + refillLoose} {t('common.pills')}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2976,7 +3269,7 @@ function AppContent() {
|
||||
<div
|
||||
key={med.id}
|
||||
className="user-med-item clickable"
|
||||
onClick={() => { setSelectedUser(null); setSelectedMed(med); }}
|
||||
onClick={() => { setSelectedUser(null); openMedDetail(med); }}
|
||||
>
|
||||
<MedicationAvatar name={med.name} imageUrl={med.imageUrl} size="sm" />
|
||||
<div className="user-med-info">
|
||||
@@ -3078,6 +3371,19 @@ function AppContent() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Schedule Lightbox - for clicking medication images in schedule */}
|
||||
{scheduleLightboxImage && (
|
||||
<div className="lightbox-overlay" onClick={() => setScheduleLightboxImage(null)}>
|
||||
<button className="lightbox-close" onClick={() => setScheduleLightboxImage(null)}>×</button>
|
||||
<img
|
||||
src={scheduleLightboxImage}
|
||||
alt="Medication"
|
||||
className="lightbox-image"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile Edit Modal */}
|
||||
{showEditModal && (
|
||||
<div className="modal-overlay" onClick={() => setShowEditModal(false)}>
|
||||
@@ -3085,11 +3391,6 @@ function AppContent() {
|
||||
<button className="modal-close" onClick={() => { setShowEditModal(false); resetForm(); }}>×</button>
|
||||
<div className="edit-modal-header">
|
||||
<h2>{editingId ? t('form.editEntry') : t('form.newEntry')}</h2>
|
||||
{editingId && (
|
||||
<button type="button" className="btn secondary small" onClick={resetForm}>
|
||||
+ {t('form.newEntry')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<form className="form-grid mobile-edit-form" onSubmit={(e) => { saveMedication(e); setShowEditModal(false); }}>
|
||||
<label className={`full ${fieldErrors.name ? 'has-error' : ''}`}>
|
||||
@@ -3166,6 +3467,45 @@ function AppContent() {
|
||||
{t('form.expiryDate')}
|
||||
<input type="date" value={form.expiryDate} onChange={(e) => setForm({ ...form, expiryDate: e.target.value })} />
|
||||
</label>
|
||||
|
||||
{/* Refill section - only shown when editing (mobile) */}
|
||||
{editingId && (
|
||||
<div className="full refill-section">
|
||||
<h4 className="refill-title">{t('refill.title')}</h4>
|
||||
<div className="refill-form-inline">
|
||||
<label>
|
||||
{t('refill.packs')}
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
value={refillPacks}
|
||||
onChange={(e) => setRefillPacks(parseInt(e.target.value) || 0)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{t('refill.loosePills')}
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
value={refillLoose}
|
||||
onChange={(e) => setRefillLoose(parseInt(e.target.value) || 0)}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="success"
|
||||
onClick={() => submitRefill(editingId)}
|
||||
disabled={(refillPacks < 1 && refillLoose < 1) || refillSaving}
|
||||
>
|
||||
{refillSaving ? t('common.saving') : t('refill.button')}
|
||||
</button>
|
||||
{(refillPacks > 0 || refillLoose > 0) && (
|
||||
<span className="refill-preview">+{refillPacks * Number(form.blistersPerPack || 0) * Number(form.pillsPerBlister || 1) + refillLoose} {t('common.pills')}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<label className={`full ${fieldErrors.notes ? 'has-error' : ''}`}>
|
||||
{t('form.notes')}
|
||||
<textarea
|
||||
|
||||
@@ -98,8 +98,8 @@
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"editEntry": "Eintrag bearbeiten",
|
||||
"newEntry": "Neuer Eintrag",
|
||||
"editEntry": "Medikament bearbeiten",
|
||||
"newEntry": "Neues Medikament",
|
||||
"badge": "Packungen + lose Tabletten",
|
||||
"commercialName": "Handelsname",
|
||||
"genericName": "Wirkstoff",
|
||||
@@ -226,7 +226,7 @@
|
||||
"pillWeight": "Tablettengewicht",
|
||||
"expiryDate": "Ablaufdatum",
|
||||
"intakeSchedule": "Einnahmeplan",
|
||||
"coverageStatus": "Reichweite",
|
||||
"coverageStatus": "Bestand",
|
||||
"daysLeft": "Tage übrig",
|
||||
"runsOut": "Aufgebraucht",
|
||||
"notes": "Notizen",
|
||||
@@ -383,5 +383,21 @@
|
||||
"importError": "Daten konnten nicht importiert werden",
|
||||
"invalidFile": "Ungültiges Dateiformat. Bitte wähle eine gültige MedAssist-Exportdatei.",
|
||||
"downloadFilename": "medassist-export"
|
||||
},
|
||||
"refill": {
|
||||
"title": "Nachfüllen",
|
||||
"packs": "Packungen hinzufügen",
|
||||
"loosePills": "Lose Tabletten hinzufügen",
|
||||
"pillsPerPack": "1 Packung = {{count}} Tabletten",
|
||||
"addToStock": "Zum Bestand hinzufügen",
|
||||
"adding": "Wird hinzugefügt...",
|
||||
"success": "{{pills}} Tabletten zum Bestand hinzugefügt",
|
||||
"history": "Nachfüll-Verlauf",
|
||||
"noHistory": "Noch keine Nachfüllungen erfasst",
|
||||
"packsAdded": "{{count}} Packung",
|
||||
"packsAdded_other": "{{count}} Packungen",
|
||||
"pillsAdded": "{{count}} Tablette",
|
||||
"pillsAdded_other": "{{count}} Tabletten",
|
||||
"button": "Nachfüllen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,8 +100,8 @@
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"editEntry": "Edit entry",
|
||||
"newEntry": "New entry",
|
||||
"editEntry": "Edit medication",
|
||||
"newEntry": "New medication",
|
||||
"badge": "Packs + loose pills",
|
||||
"commercialName": "Commercial Name",
|
||||
"genericName": "Generic Name",
|
||||
@@ -385,5 +385,21 @@
|
||||
"importError": "Failed to import data",
|
||||
"invalidFile": "Invalid file format. Please select a valid MedAssist export file.",
|
||||
"downloadFilename": "medassist-export"
|
||||
},
|
||||
"refill": {
|
||||
"title": "Refill",
|
||||
"packs": "Packs to add",
|
||||
"loosePills": "Loose pills to add",
|
||||
"pillsPerPack": "1 pack = {{count}} pills",
|
||||
"addToStock": "Add to Stock",
|
||||
"adding": "Adding...",
|
||||
"success": "Added {{pills}} pills to stock",
|
||||
"history": "Refill History",
|
||||
"noHistory": "No refills recorded yet",
|
||||
"packsAdded": "{{count}} pack",
|
||||
"packsAdded_other": "{{count}} packs",
|
||||
"pillsAdded": "{{count}} pill",
|
||||
"pillsAdded_other": "{{count}} pills",
|
||||
"button": "Refill"
|
||||
}
|
||||
}
|
||||
|
||||
+384
-75
@@ -431,6 +431,46 @@ button.secondary:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
/* Success button (Refill, etc.) */
|
||||
button.success {
|
||||
background: var(--success);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
button.success:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
button.success:disabled {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-tertiary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Primary/Accent button (New entry, Add intake, etc.) */
|
||||
button.primary {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
button.primary:hover {
|
||||
background: var(--accent-light);
|
||||
}
|
||||
button.primary:disabled {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-tertiary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Info button (Edit, secondary actions) */
|
||||
button.info {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
button.info:hover {
|
||||
background: #60a5fa;
|
||||
}
|
||||
|
||||
/* Ghost button (Cancel, etc.) */
|
||||
button.ghost {
|
||||
background: transparent;
|
||||
@@ -1388,13 +1428,21 @@ textarea.auto-resize {
|
||||
}
|
||||
|
||||
.setting-row.language-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 1.5rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.setting-row.language-row .setting-label {
|
||||
flex: 0 0 auto;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.language-select {
|
||||
width: auto;
|
||||
min-width: 160px;
|
||||
flex: 1 1 auto;
|
||||
min-width: 140px;
|
||||
max-width: 200px;
|
||||
padding: 0.6rem 2rem 0.6rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
@@ -1663,11 +1711,42 @@ textarea.auto-resize {
|
||||
}
|
||||
|
||||
.info-tooltip:hover::after,
|
||||
.info-tooltip:hover::before {
|
||||
.info-tooltip:hover::before,
|
||||
.info-tooltip:focus::after,
|
||||
.info-tooltip:focus::before,
|
||||
.info-tooltip.tooltip-active::after,
|
||||
.info-tooltip.tooltip-active::before {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Mobile tooltip - disable hover, use click only */
|
||||
@media (max-width: 640px) {
|
||||
.info-tooltip:hover::after,
|
||||
.info-tooltip:hover::before {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.info-tooltip.tooltip-active::after {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
bottom: auto;
|
||||
right: auto;
|
||||
transform: translate(-50%, -50%);
|
||||
max-width: calc(100vw - 32px);
|
||||
width: max-content;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.info-tooltip::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Channels Overview */
|
||||
.channels-overview {
|
||||
display: flex;
|
||||
@@ -2090,10 +2169,13 @@ textarea.auto-resize {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Compact Setting Row */
|
||||
/* Compact Setting Row - for inline toggles without card styling */
|
||||
.setting-row.compact {
|
||||
padding: 0.75rem 1rem;
|
||||
padding: 0.75rem 0;
|
||||
margin-top: 0.5rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.setting-row.compact .setting-label {
|
||||
@@ -2284,6 +2366,9 @@ textarea.auto-resize {
|
||||
.med-avatar-sm.med-avatar-initials { font-size: 0.65em; }
|
||||
.med-avatar-lg.med-avatar-initials { font-size: 1.1em; }
|
||||
|
||||
.med-avatar.clickable { cursor: pointer; transition: transform 0.15s, box-shadow 0.15s; }
|
||||
.med-avatar.clickable:hover { transform: scale(1.1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); }
|
||||
|
||||
/* Table/Timeline cells with avatar */
|
||||
.cell-with-avatar {
|
||||
display: flex;
|
||||
@@ -2885,14 +2970,22 @@ textarea.auto-resize {
|
||||
}
|
||||
|
||||
.lightbox-image {
|
||||
max-width: 90vw;
|
||||
max-height: 85vh;
|
||||
max-width: 50vw;
|
||||
max-height: 50vh;
|
||||
object-fit: contain;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||||
animation: zoomIn 0.3s ease;
|
||||
}
|
||||
|
||||
/* Mobile: larger lightbox image */
|
||||
@media (max-width: 768px) {
|
||||
.lightbox-image {
|
||||
max-width: 90vw;
|
||||
max-height: 70vh;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes zoomIn {
|
||||
from { opacity: 0; transform: scale(0.8); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
@@ -2932,6 +3025,22 @@ textarea.auto-resize {
|
||||
|
||||
.med-detail-footer {
|
||||
padding: 1rem 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.med-detail-footer > button {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.med-detail-footer .footer-actions {
|
||||
flex: 1 1 auto;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.med-detail-footer button {
|
||||
padding: 0.6rem 0.8rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.med-detail-grid {
|
||||
@@ -3283,7 +3392,17 @@ h3 .reminder-icon.info-tooltip {
|
||||
box-shadow: 0 16px 48px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.user-menu:hover .user-dropdown {
|
||||
/* Only use hover on devices that support it (not touch) */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.user-menu:hover .user-dropdown {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Click-based open for all devices */
|
||||
.user-menu.open .user-dropdown {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0) scale(1);
|
||||
@@ -3717,6 +3836,215 @@ h3 .reminder-icon.info-tooltip {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
Refill Modal & History
|
||||
============================================================================= */
|
||||
.refill-modal {
|
||||
max-width: 500px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.refill-modal h2 {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.refill-med-name {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.refill-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.refill-form label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.refill-form input {
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-input);
|
||||
color: var(--text-primary);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.refill-footer-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.refill-footer-right .refill-preview {
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
/* Refill modal footer mobile */
|
||||
@media (max-width: 640px) {
|
||||
.refill-modal .modal-footer {
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.refill-modal .modal-footer > button,
|
||||
.refill-modal .modal-footer .refill-footer-right {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.refill-modal .modal-footer .refill-footer-right {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.refill-modal .modal-footer .refill-footer-right button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Inline refill form in edit modal */
|
||||
.refill-form-inline {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.refill-form-inline label {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.refill-form-inline label input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.refill-form-inline button {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 0;
|
||||
align-self: flex-end;
|
||||
height: 42px;
|
||||
padding: 0 1rem;
|
||||
min-width: 110px;
|
||||
}
|
||||
|
||||
.refill-preview {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 42px;
|
||||
padding: 0 0.75rem;
|
||||
background: transparent;
|
||||
border: 1px dashed var(--success);
|
||||
border-radius: 6px;
|
||||
color: var(--success);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
align-self: flex-end;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.refill-section {
|
||||
border-left: 3px solid var(--success);
|
||||
background: var(--bg-secondary);
|
||||
margin-top: 1.5rem;
|
||||
padding: 1rem 1rem 1rem 1.25rem;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
.refill-section .refill-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 0.75rem 0;
|
||||
}
|
||||
|
||||
.refill-section .refill-form-inline button {
|
||||
background: var(--success);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.refill-section .refill-form-inline button:hover:not(:disabled) {
|
||||
background: var(--success-hover, #3aa865);
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.refill-section .refill-form-inline button:disabled {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-tertiary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Clickable section header (for expand/collapse) */
|
||||
.section-header-clickable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.section-header-clickable:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Refill history in detail modal */
|
||||
.refill-history-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.refill-history-header .collapse-icon {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.refill-history-header .refill-count {
|
||||
font-weight: normal;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.refill-history-list {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.refill-history-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
}
|
||||
|
||||
.refill-history-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.refill-date {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.refill-details {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
/* Nested modal overlay */
|
||||
.modal-overlay.nested {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
Shared Schedule Page (Public)
|
||||
============================================================================= */
|
||||
@@ -4007,78 +4335,59 @@ h3 .reminder-icon.info-tooltip {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Export/Import Section */
|
||||
.card:has(.export-import-grid) {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.card:has(.export-import-grid) .card-head {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.export-import-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.export-import-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.export-import-card {
|
||||
/* Action Cards (for Export/Import etc.) - similar to radio-card */
|
||||
.action-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.25rem;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-secondary);
|
||||
border-radius: var(--card-radius);
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.export-import-card h3 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.export-import-desc {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.export-import-card button,
|
||||
.export-import-file-btn {
|
||||
margin-top: auto;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.export-import-file-btn {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
padding: 0.7rem 1.25rem;
|
||||
border-radius: var(--btn-radius);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-secondary);
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
border-radius: 12px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.export-import-file-btn:hover {
|
||||
background: var(--bg-hover);
|
||||
.action-card:hover {
|
||||
border-color: var(--border-primary);
|
||||
}
|
||||
|
||||
.export-import-file-btn input {
|
||||
display: none;
|
||||
.action-card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.action-card-title {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.action-card-desc {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.action-card button,
|
||||
.action-card .btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.action-card {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.action-card button,
|
||||
.action-card .btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user