@jjlmoya/utils-pets 1.26.0 → 1.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/category/index.ts +2 -1
- package/src/data.ts +2 -0
- package/src/entries.ts +4 -1
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +12 -10
- package/src/tool/petAge/component.astro +1 -1
- package/src/tool/petMedicationSchedulePlanner/bibliography.astro +6 -0
- package/src/tool/petMedicationSchedulePlanner/bibliography.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/component.astro +45 -0
- package/src/tool/petMedicationSchedulePlanner/controller.ts +93 -0
- package/src/tool/petMedicationSchedulePlanner/dom-views.ts +57 -0
- package/src/tool/petMedicationSchedulePlanner/entry.ts +26 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/content.ts +57 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/de.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/en.ts +7 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/es.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/fr.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/id.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/it.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/ja.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/ko.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/nl.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/pl.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/pt.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/ru.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/sv.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/tr.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/zh.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/index.ts +12 -0
- package/src/tool/petMedicationSchedulePlanner/logic.test.ts +24 -0
- package/src/tool/petMedicationSchedulePlanner/logic.ts +118 -0
- package/src/tool/petMedicationSchedulePlanner/seo.astro +11 -0
- package/src/tool/petMedicationSchedulePlanner/storage.ts +32 -0
- package/src/tool/petMedicationSchedulePlanner/ui.ts +43 -0
- package/src/tools.ts +3 -0
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -5,10 +5,11 @@ import { petGestation } from '../tool/petGestation/entry';
|
|
|
5
5
|
import { petToxicity } from '../tool/petToxicity/entry';
|
|
6
6
|
import { petWaterIntake } from '../tool/petWaterIntake/entry';
|
|
7
7
|
import { petCarrierCrateSizePlanner } from '../tool/petCarrierCrateSizePlanner/entry';
|
|
8
|
+
import { petMedicationSchedulePlanner } from '../tool/petMedicationSchedulePlanner/entry';
|
|
8
9
|
|
|
9
10
|
export const petsCategory: PetCategoryEntry = {
|
|
10
11
|
icon: 'mdi:paw',
|
|
11
|
-
tools: [petAge, petRation, petGestation, petToxicity, petWaterIntake, petCarrierCrateSizePlanner],
|
|
12
|
+
tools: [petAge, petRation, petGestation, petToxicity, petWaterIntake, petCarrierCrateSizePlanner, petMedicationSchedulePlanner],
|
|
12
13
|
i18n: {
|
|
13
14
|
en: () => import('./i18n/en').then((m) => m.content),
|
|
14
15
|
es: () => import('./i18n/es').then((m) => m.content),
|
package/src/data.ts
CHANGED
|
@@ -3,11 +3,13 @@ export { petAge } from './tool/petAge';
|
|
|
3
3
|
export { petRation } from './tool/petRation';
|
|
4
4
|
export { petGestation } from './tool/petGestation';
|
|
5
5
|
export { petToxicity } from './tool/petToxicity';
|
|
6
|
+
export { petMedicationSchedulePlanner } from './tool/petMedicationSchedulePlanner';
|
|
6
7
|
|
|
7
8
|
export type { PetAgeUI, PetAgeLocaleContent } from './tool/petAge';
|
|
8
9
|
export type { PetRationUI, PetRationLocaleContent } from './tool/petRation';
|
|
9
10
|
export type { PetGestationUI, PetGestationLocaleContent } from './tool/petGestation';
|
|
10
11
|
export type { PetToxicityUI, PetToxicityLocaleContent } from './tool/petToxicity';
|
|
12
|
+
export type { PetMedicationSchedulePlannerUI, PetMedicationSchedulePlannerLocaleContent } from './tool/petMedicationSchedulePlanner';
|
|
11
13
|
|
|
12
14
|
export type {
|
|
13
15
|
KnownLocale,
|
package/src/entries.ts
CHANGED
|
@@ -8,6 +8,8 @@ export { petWaterIntake } from './tool/petWaterIntake/entry';
|
|
|
8
8
|
export type { PetWaterIntakeUI, PetWaterIntakeLocaleContent } from './tool/petWaterIntake/entry';
|
|
9
9
|
export { petCarrierCrateSizePlanner } from './tool/petCarrierCrateSizePlanner/entry';
|
|
10
10
|
export type { PetCarrierCrateSizePlannerUI, PetCarrierCrateSizePlannerLocaleContent } from './tool/petCarrierCrateSizePlanner/entry';
|
|
11
|
+
export { petMedicationSchedulePlanner } from './tool/petMedicationSchedulePlanner/entry';
|
|
12
|
+
export type { PetMedicationSchedulePlannerUI, PetMedicationSchedulePlannerLocaleContent } from './tool/petMedicationSchedulePlanner/entry';
|
|
11
13
|
export { petsCategory } from './category';
|
|
12
14
|
import { petAge } from './tool/petAge/entry';
|
|
13
15
|
import { petRation } from './tool/petRation/entry';
|
|
@@ -15,4 +17,5 @@ import { petGestation } from './tool/petGestation/entry';
|
|
|
15
17
|
import { petToxicity } from './tool/petToxicity/entry';
|
|
16
18
|
import { petWaterIntake } from './tool/petWaterIntake/entry';
|
|
17
19
|
import { petCarrierCrateSizePlanner } from './tool/petCarrierCrateSizePlanner/entry';
|
|
18
|
-
|
|
20
|
+
import { petMedicationSchedulePlanner } from './tool/petMedicationSchedulePlanner/entry';
|
|
21
|
+
export const ALL_ENTRIES = [petAge, petRation, petGestation, petToxicity, petWaterIntake, petCarrierCrateSizePlanner, petMedicationSchedulePlanner];
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { petAge, PET_AGE_TOOL } from './tool/petAge';
|
|
|
2
2
|
export { petRation, PET_RATION_TOOL } from './tool/petRation';
|
|
3
3
|
export { petGestation, PET_GESTATION_TOOL } from './tool/petGestation';
|
|
4
4
|
export { petCarrierCrateSizePlanner, PET_CARRIER_CRATE_SIZE_PLANNER_TOOL } from './tool/petCarrierCrateSizePlanner';
|
|
5
|
+
export { petMedicationSchedulePlanner, PET_MEDICATION_SCHEDULE_PLANNER_TOOL } from './tool/petMedicationSchedulePlanner';
|
|
5
6
|
|
|
6
7
|
export { petsCategory } from './category';
|
|
7
8
|
export const PetsCategorySEO = () => import('./category/seo.astro').then((m) => m.default);
|
|
@@ -33,12 +33,14 @@ function extractSectionText(section: any): string {
|
|
|
33
33
|
return text;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function countWords(text: string): number {
|
|
37
|
-
|
|
38
|
-
.replace(/<[^>]*>/g, '')
|
|
39
|
-
.trim()
|
|
40
|
-
.split(/\s+/)
|
|
41
|
-
.filter((w) => w.length > 0).length;
|
|
36
|
+
function countWords(text: string): number {
|
|
37
|
+
const words = text
|
|
38
|
+
.replace(/<[^>]*>/g, '')
|
|
39
|
+
.trim()
|
|
40
|
+
.split(/\s+/)
|
|
41
|
+
.filter((w) => w.length > 0).length;
|
|
42
|
+
const cjkCharacters = (text.match(/[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uac00-\ud7af]/g) ?? []).length;
|
|
43
|
+
return Math.max(words, cjkCharacters);
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
describe('Tool Validation Suite', () => {
|
|
@@ -71,7 +73,7 @@ describe('Tool Validation Suite', () => {
|
|
|
71
73
|
expect(content.slug).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/);
|
|
72
74
|
|
|
73
75
|
if (locale === 'es') {
|
|
74
|
-
const validSlugs = ['calculadora-edad-mascotas', 'calculadora-racion-diaria-mascotas', 'calculadora-gestacion-mascotas', 'buscador-alimentos-toxicos-perros-gatos', 'calculadora-agua-diaria-perros-gatos', 'planificador-dimensiones-transportin-mascotas'];
|
|
76
|
+
const validSlugs = ['calculadora-edad-mascotas', 'calculadora-racion-diaria-mascotas', 'calculadora-gestacion-mascotas', 'buscador-alimentos-toxicos-perros-gatos', 'calculadora-agua-diaria-perros-gatos', 'planificador-dimensiones-transportin-mascotas', 'planificador-horario-medicacion-mascota'];
|
|
75
77
|
expect(validSlugs).toContain(content.slug);
|
|
76
78
|
}
|
|
77
79
|
});
|
|
@@ -96,12 +98,12 @@ describe('Tool Validation Suite', () => {
|
|
|
96
98
|
});
|
|
97
99
|
|
|
98
100
|
describe('Library Registration', () => {
|
|
99
|
-
it('should have
|
|
100
|
-
expect(ALL_TOOLS.length).toBe(
|
|
101
|
+
it('should have 7 tools in ALL_TOOLS', () => {
|
|
102
|
+
expect(ALL_TOOLS.length).toBe(7);
|
|
101
103
|
});
|
|
102
104
|
|
|
103
105
|
it('should have all tools in petsCategory', () => {
|
|
104
|
-
expect(petsCategory.tools.length).toBe(
|
|
106
|
+
expect(petsCategory.tools.length).toBe(7);
|
|
105
107
|
ALL_TOOLS.forEach(({ entry }) => {
|
|
106
108
|
const exists = petsCategory.tools.some((t: any) => t.id === entry.id);
|
|
107
109
|
expect(exists).toBe(true);
|
|
@@ -254,7 +254,7 @@ const { ui } = Astro.props;
|
|
|
254
254
|
const url = generateShareUrl(state);
|
|
255
255
|
try {
|
|
256
256
|
await navigator.clipboard.writeText(url);
|
|
257
|
-
if (els.shareText) els.shareText.textContent = uiData.shareSuccess;
|
|
257
|
+
if (els.shareText) els.shareText.textContent = uiData.shareSuccess || "";
|
|
258
258
|
if (els.shareBtn) els.shareBtn.classList.add("age-btn-success");
|
|
259
259
|
setTimeout(() => (window.location.href = url), 800);
|
|
260
260
|
} catch {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
|
+
{ name: 'American Veterinary Medical Association: Medication Safety', url: 'https://www.avma.org/resources/pet-owners/petcare/medication-safety' },
|
|
5
|
+
{ name: 'FDA: Giving Medication to Your Pet', url: 'https://www.fda.gov/animal-veterinary/animal-health-literacy/giving-medication-your-pet' },
|
|
6
|
+
];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { PetMedicationSchedulePlannerUI } from './index';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
ui: PetMedicationSchedulePlannerUI;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { ui } = Astro.props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<div class="pet-medication-schedule-planner" data-schedule-root>
|
|
12
|
+
<script is:inline type="application/json" data-medication-ui set:html={JSON.stringify(ui)}></script>
|
|
13
|
+
<header class="medication-hero">
|
|
14
|
+
<div><p class="medication-kicker">{ui.heroEyebrow}</p><p class="medication-hint">{ui.heroHint}</p></div>
|
|
15
|
+
<span class="local-badge">{ui.localOnlyLabel}</span>
|
|
16
|
+
</header>
|
|
17
|
+
<div class="medication-workbench">
|
|
18
|
+
<form class="medication-form" onsubmit="return false">
|
|
19
|
+
<div class="form-heading"><span>01</span><div><p>{ui.scheduleTitle}</p><h2>{ui.medicationNameLabel}</h2></div></div>
|
|
20
|
+
<label class="full-field"><span>{ui.medicationNameLabel}</span><input data-input data-medication-name type="text" placeholder={ui.medicationNamePlaceholder} autocomplete="off" /></label>
|
|
21
|
+
<div class="two-fields"><label><span>{ui.startDateLabel}</span><input data-input data-start-date type="date" /></label><label><span>{ui.startTimeLabel}</span><input data-input data-start-time type="time" /></label></div>
|
|
22
|
+
<fieldset class="mode-field"><legend>{ui.scheduleModeLabel}</legend><div class="mode-buttons"><button type="button" data-mode="interval" aria-pressed="true">{ui.intervalMode}</button><button type="button" data-mode="times" aria-pressed="false">{ui.timesMode}</button></div></fieldset>
|
|
23
|
+
<label data-interval-wrap><span>{ui.intervalHoursLabel}</span><input data-input data-interval-hours type="number" min="1" max="24" step="1" /></label>
|
|
24
|
+
<label data-times-wrap hidden><span>{ui.timesLabel}<small>{ui.timesHint}</small></span><input data-input data-daily-times type="text" /></label>
|
|
25
|
+
<label><span>{ui.durationLabel}</span><span class="with-suffix"><input data-input data-duration-days type="number" min="1" max="30" step="1" /><b>{ui.durationUnit}</b></span></label>
|
|
26
|
+
<label><span>{ui.instructionsLabel}</span><textarea data-input data-instructions rows="3" placeholder={ui.instructionsPlaceholder}></textarea></label>
|
|
27
|
+
<p class="form-error" data-error role="alert"></p>
|
|
28
|
+
<button class="reset-button" type="button" data-reset>{ui.reset}</button>
|
|
29
|
+
</form>
|
|
30
|
+
<section class="medication-result" data-result aria-live="polite">
|
|
31
|
+
<div class="result-top"><div><p class="result-kicker">{ui.scheduleSummary}</p><h2>{ui.scheduleTitle}</h2></div><div class="next-dose"><span>{ui.nextDoseLabel}</span><strong data-next-dose>{ui.noNextDose}</strong></div></div>
|
|
32
|
+
<div class="progress-line"><span data-completed-count></span><span class="progress-rule"></span></div>
|
|
33
|
+
<div class="schedule-list" data-schedule-list aria-label={ui.scheduleIllustration}></div>
|
|
34
|
+
<p class="empty-schedule" data-empty-schedule>{ui.emptySchedule}</p>
|
|
35
|
+
</section>
|
|
36
|
+
</div>
|
|
37
|
+
<aside class="medication-safety"><strong>{ui.safetyTitle}</strong><span>{ui.safetyText}</span></aside>
|
|
38
|
+
<p class="medication-method"><strong>{ui.methodTitle}</strong> {ui.methodText}</p>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<script>
|
|
42
|
+
import { initMedicationSchedulePlanner } from './controller';
|
|
43
|
+
const root = document.querySelector<HTMLElement>('[data-schedule-root]');
|
|
44
|
+
if (root) initMedicationSchedulePlanner(root);
|
|
45
|
+
</script>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { generateSchedule, type ScheduleMode } from './logic';
|
|
2
|
+
import { clearMedicationState, loadMedicationState, saveMedicationState, type MedicationStorageState } from './storage';
|
|
3
|
+
import { renderSchedule } from './dom-views';
|
|
4
|
+
import type { PetMedicationSchedulePlannerUI } from './ui';
|
|
5
|
+
|
|
6
|
+
function todayValue(): string {
|
|
7
|
+
const now = new Date();
|
|
8
|
+
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function defaultState(): MedicationStorageState {
|
|
12
|
+
return { medicationName: '', startDate: todayValue(), startTime: '08:00', mode: 'interval', intervalHours: 8, dailyTimes: '08:00, 16:00, 00:00', durationDays: 5, instructions: '', completedIds: [] };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function readUI(root: HTMLElement): PetMedicationSchedulePlannerUI {
|
|
16
|
+
const script = root.querySelector<HTMLScriptElement>('[data-medication-ui]');
|
|
17
|
+
return JSON.parse(script?.textContent || '{}') as PetMedicationSchedulePlannerUI;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function mergeState(stored: Partial<MedicationStorageState>): MedicationStorageState {
|
|
21
|
+
return { ...defaultState(), ...stored, completedIds: Array.isArray(stored.completedIds) ? stored.completedIds : [] };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readForm(root: HTMLElement, state: MedicationStorageState): void {
|
|
25
|
+
const read = (selector: string): string => root.querySelector<HTMLInputElement | HTMLTextAreaElement>(selector)?.value ?? '';
|
|
26
|
+
state.medicationName = read('[data-medication-name]');
|
|
27
|
+
state.startDate = read('[data-start-date]');
|
|
28
|
+
state.startTime = read('[data-start-time]');
|
|
29
|
+
state.intervalHours = Number(read('[data-interval-hours]'));
|
|
30
|
+
state.dailyTimes = read('[data-daily-times]');
|
|
31
|
+
state.durationDays = Number(read('[data-duration-days]'));
|
|
32
|
+
state.instructions = read('[data-instructions]');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function syncForm(root: HTMLElement, state: MedicationStorageState): void {
|
|
36
|
+
const set = (selector: string, value: string | number): void => { const input = root.querySelector<HTMLInputElement | HTMLTextAreaElement>(selector); if (input) input.value = String(value); };
|
|
37
|
+
set('[data-medication-name]', state.medicationName);
|
|
38
|
+
set('[data-start-date]', state.startDate);
|
|
39
|
+
set('[data-start-time]', state.startTime);
|
|
40
|
+
set('[data-interval-hours]', state.intervalHours);
|
|
41
|
+
set('[data-daily-times]', state.dailyTimes);
|
|
42
|
+
set('[data-duration-days]', state.durationDays);
|
|
43
|
+
set('[data-instructions]', state.instructions);
|
|
44
|
+
root.querySelectorAll<HTMLButtonElement>('[data-mode]').forEach((button) => { const active = button.dataset.mode === state.mode; button.classList.toggle('is-active', active); button.setAttribute('aria-pressed', String(active)); });
|
|
45
|
+
const interval = root.querySelector<HTMLElement>('[data-interval-wrap]');
|
|
46
|
+
const times = root.querySelector<HTMLElement>('[data-times-wrap]');
|
|
47
|
+
if (interval) interval.hidden = state.mode !== 'interval';
|
|
48
|
+
if (times) times.hidden = state.mode !== 'times';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function renderEmptyState(root: HTMLElement, error: HTMLElement | null): void {
|
|
52
|
+
if (error) error.textContent = '';
|
|
53
|
+
root.querySelector<HTMLElement>('[data-result]')?.classList.remove('is-ready');
|
|
54
|
+
root.querySelector<HTMLElement>('[data-empty-schedule]')?.removeAttribute('hidden');
|
|
55
|
+
root.querySelector<HTMLElement>('[data-schedule-list]')?.replaceChildren();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function renderValidState(root: HTMLElement, state: MedicationStorageState, ui: PetMedicationSchedulePlannerUI, error: HTMLElement | null): void {
|
|
59
|
+
try {
|
|
60
|
+
const doses = generateSchedule(state);
|
|
61
|
+
saveMedicationState(state);
|
|
62
|
+
renderSchedule(root, doses, { completedIds: state.completedIds, now: new Date() }, ui);
|
|
63
|
+
if (error) error.textContent = '';
|
|
64
|
+
root.querySelector<HTMLElement>('[data-result]')?.classList.add('is-ready');
|
|
65
|
+
} catch {
|
|
66
|
+
if (error) error.textContent = ui.invalidInput;
|
|
67
|
+
root.querySelector<HTMLElement>('[data-result]')?.classList.remove('is-ready');
|
|
68
|
+
const empty = root.querySelector<HTMLElement>('[data-empty-schedule]');
|
|
69
|
+
if (empty) empty.hidden = false;
|
|
70
|
+
root.querySelector<HTMLElement>('[data-schedule-list]')?.replaceChildren();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function render(root: HTMLElement, state: MedicationStorageState, ui: PetMedicationSchedulePlannerUI): void {
|
|
75
|
+
readForm(root, state);
|
|
76
|
+
const error = root.querySelector<HTMLElement>('[data-error]');
|
|
77
|
+
if (!state.medicationName.trim()) {
|
|
78
|
+
renderEmptyState(root, error);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
renderValidState(root, state, ui, error);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function initMedicationSchedulePlanner(root: HTMLElement): void {
|
|
85
|
+
const ui = readUI(root);
|
|
86
|
+
const state = mergeState(loadMedicationState());
|
|
87
|
+
syncForm(root, state);
|
|
88
|
+
root.querySelectorAll<HTMLButtonElement>('[data-mode]').forEach((button) => button.addEventListener('click', () => { state.mode = (button.dataset.mode || 'interval') as ScheduleMode; syncForm(root, state); render(root, state, ui); }));
|
|
89
|
+
root.querySelectorAll<HTMLInputElement | HTMLTextAreaElement>('[data-input]').forEach((input) => input.addEventListener('input', () => { state.completedIds = []; render(root, state, ui); }));
|
|
90
|
+
root.querySelector<HTMLElement>('[data-schedule-list]')?.addEventListener('click', (event) => { const button = (event.target as HTMLElement).closest<HTMLButtonElement>('[data-dose-id]'); if (!button) return; const id = button.dataset.doseId; if (!id) return; state.completedIds = state.completedIds.includes(id) ? state.completedIds.filter((item) => item !== id) : [...state.completedIds, id]; render(root, state, ui); });
|
|
91
|
+
root.querySelector<HTMLButtonElement>('[data-reset]')?.addEventListener('click', () => { clearMedicationState(); Object.assign(state, defaultState()); syncForm(root, state); render(root, state, ui); });
|
|
92
|
+
render(root, state, ui);
|
|
93
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { formatDay, formatTime, type ScheduledDose } from './logic';
|
|
2
|
+
import type { PetMedicationSchedulePlannerUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export interface RenderState {
|
|
5
|
+
completedIds: string[];
|
|
6
|
+
now: Date;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function setText(root: HTMLElement, selector: string, value: string): void {
|
|
10
|
+
const element = root.querySelector<HTMLElement>(selector);
|
|
11
|
+
if (element) element.textContent = value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function createDoseButton(dose: ScheduledDose, state: RenderState, ui: PetMedicationSchedulePlannerUI): HTMLButtonElement {
|
|
15
|
+
const completed = state.completedIds.includes(dose.id);
|
|
16
|
+
const button = document.createElement('button');
|
|
17
|
+
button.type = 'button';
|
|
18
|
+
button.className = `dose-row${completed ? ' is-complete' : ''}`;
|
|
19
|
+
button.dataset.doseId = dose.id;
|
|
20
|
+
button.setAttribute('aria-pressed', String(completed));
|
|
21
|
+
button.innerHTML = '<span class="dose-check" aria-hidden="true"></span><span class="dose-time"></span><span class="dose-state"></span>';
|
|
22
|
+
const time = button.querySelector<HTMLElement>('.dose-time');
|
|
23
|
+
if (time) time.textContent = formatTime(dose.date, ui.dateLocale);
|
|
24
|
+
const status = button.querySelector<HTMLElement>('.dose-state');
|
|
25
|
+
if (status) {
|
|
26
|
+
if (completed) status.textContent = ui.completed;
|
|
27
|
+
else if (dose.date <= state.now) status.textContent = ui.due;
|
|
28
|
+
else status.textContent = ui.upcoming;
|
|
29
|
+
}
|
|
30
|
+
button.title = completed ? ui.markUndone : ui.markDone;
|
|
31
|
+
return button;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function renderSchedule(root: HTMLElement, doses: ScheduledDose[], state: RenderState, ui: PetMedicationSchedulePlannerUI): void {
|
|
35
|
+
const list = root.querySelector<HTMLElement>('[data-schedule-list]');
|
|
36
|
+
if (!list) return;
|
|
37
|
+
list.replaceChildren();
|
|
38
|
+
const groups = new Map<string, ScheduledDose[]>();
|
|
39
|
+
doses.forEach((dose) => groups.set(dose.dateKey, [...(groups.get(dose.dateKey) ?? []), dose]));
|
|
40
|
+
groups.forEach((group) => {
|
|
41
|
+
const first = group[0];
|
|
42
|
+
if (!first) return;
|
|
43
|
+
const section = document.createElement('section');
|
|
44
|
+
section.className = 'dose-day';
|
|
45
|
+
const heading = document.createElement('h3');
|
|
46
|
+
heading.textContent = formatDay(first.date, ui.dateLocale);
|
|
47
|
+
section.append(heading);
|
|
48
|
+
group.forEach((dose) => section.append(createDoseButton(dose, state, ui)));
|
|
49
|
+
list.append(section);
|
|
50
|
+
});
|
|
51
|
+
const completed = doses.filter((dose) => state.completedIds.includes(dose.id)).length;
|
|
52
|
+
setText(root, '[data-completed-count]', ui.completedCount.replace('{done}', String(completed)).replace('{total}', String(doses.length)));
|
|
53
|
+
const next = doses.find((dose) => !state.completedIds.includes(dose.id) && dose.date >= state.now) ?? doses.find((dose) => !state.completedIds.includes(dose.id));
|
|
54
|
+
setText(root, '[data-next-dose]', next ? `${formatDay(next.date, ui.dateLocale)} · ${formatTime(next.date, ui.dateLocale)}` : ui.noNextDose);
|
|
55
|
+
const empty = root.querySelector<HTMLElement>('[data-empty-schedule]');
|
|
56
|
+
if (empty) empty.hidden = doses.length > 0;
|
|
57
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PetToolEntry } from '../../types';
|
|
2
|
+
import type { PetMedicationSchedulePlannerUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export type { PetMedicationSchedulePlannerLocaleContent, PetMedicationSchedulePlannerUI } from './ui';
|
|
5
|
+
|
|
6
|
+
export const petMedicationSchedulePlanner: PetToolEntry<PetMedicationSchedulePlannerUI> = {
|
|
7
|
+
id: 'pet-medication-schedule-planner',
|
|
8
|
+
icons: { bg: 'mdi:paw', fg: 'mdi:calendar-check' },
|
|
9
|
+
i18n: {
|
|
10
|
+
de: () => import('./i18n/de').then((module) => module.content),
|
|
11
|
+
en: () => import('./i18n/en').then((module) => module.content),
|
|
12
|
+
es: () => import('./i18n/es').then((module) => module.content),
|
|
13
|
+
fr: () => import('./i18n/fr').then((module) => module.content),
|
|
14
|
+
id: () => import('./i18n/id').then((module) => module.content),
|
|
15
|
+
it: () => import('./i18n/it').then((module) => module.content),
|
|
16
|
+
ja: () => import('./i18n/ja').then((module) => module.content),
|
|
17
|
+
ko: () => import('./i18n/ko').then((module) => module.content),
|
|
18
|
+
nl: () => import('./i18n/nl').then((module) => module.content),
|
|
19
|
+
pl: () => import('./i18n/pl').then((module) => module.content),
|
|
20
|
+
pt: () => import('./i18n/pt').then((module) => module.content),
|
|
21
|
+
ru: () => import('./i18n/ru').then((module) => module.content),
|
|
22
|
+
sv: () => import('./i18n/sv').then((module) => module.content),
|
|
23
|
+
tr: () => import('./i18n/tr').then((module) => module.content),
|
|
24
|
+
zh: () => import('./i18n/zh').then((module) => module.content),
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { FAQPage, HowTo, SoftwareApplication, WithContext } from 'schema-dts';
|
|
2
|
+
import { bibliography } from '../bibliography';
|
|
3
|
+
import type { PetMedicationSchedulePlannerLocaleContent, PetMedicationSchedulePlannerUI } from '../ui';
|
|
4
|
+
|
|
5
|
+
type MedicationUIFields = Pick<PetMedicationSchedulePlannerUI, 'dateLocale' | 'heroEyebrow' | 'heroHint' | 'medicationNameLabel' | 'medicationNamePlaceholder' | 'startDateLabel' | 'startTimeLabel' | 'scheduleModeLabel' | 'intervalMode' | 'timesMode' | 'intervalHoursLabel' | 'timesLabel' | 'timesHint' | 'durationLabel' | 'durationUnit' | 'instructionsLabel' | 'instructionsPlaceholder' | 'reset' | 'scheduleTitle' | 'scheduleSummary' | 'nextDoseLabel' | 'noNextDose' | 'completedCount' | 'markDone' | 'markUndone' | 'completed' | 'upcoming' | 'due' | 'emptySchedule' | 'invalidInput' | 'localOnlyLabel' | 'safetyTitle' | 'safetyText' | 'methodTitle' | 'methodText' | 'scheduleIllustration'>;
|
|
6
|
+
|
|
7
|
+
export interface MedicationCopy extends MedicationUIFields {
|
|
8
|
+
slug: string;
|
|
9
|
+
title: string;
|
|
10
|
+
description: string;
|
|
11
|
+
summary: string[];
|
|
12
|
+
seoTitle1: string;
|
|
13
|
+
seoIntro: string;
|
|
14
|
+
seoTitle2: string;
|
|
15
|
+
seoMethod: string;
|
|
16
|
+
seoTitle3: string;
|
|
17
|
+
seoSafety: string;
|
|
18
|
+
tipTitle: string;
|
|
19
|
+
tipText: string;
|
|
20
|
+
faq: { question: string; answer: string }[];
|
|
21
|
+
howTo: { name: string; text: string }[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createContent(copy: MedicationCopy): PetMedicationSchedulePlannerLocaleContent {
|
|
25
|
+
const ui = copy as unknown as PetMedicationSchedulePlannerUI;
|
|
26
|
+
const faq = copy.faq;
|
|
27
|
+
const howTo = copy.howTo;
|
|
28
|
+
let slug = copy.slug;
|
|
29
|
+
if (['ja-JP', 'ko-KR', 'zh-CN'].includes(copy.dateLocale)) slug = 'pet-medication-schedule-planner';
|
|
30
|
+
const schemas = [
|
|
31
|
+
{ '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: copy.title, description: copy.description, applicationCategory: 'LifestyleApplication', operatingSystem: 'Web', offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' } } as WithContext<SoftwareApplication>,
|
|
32
|
+
{ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) } as WithContext<FAQPage>,
|
|
33
|
+
{ '@context': 'https://schema.org', '@type': 'HowTo', name: copy.title, step: howTo.map((step) => ({ '@type': 'HowToStep', name: step.name, text: step.text })) } as WithContext<HowTo>,
|
|
34
|
+
];
|
|
35
|
+
return {
|
|
36
|
+
slug,
|
|
37
|
+
title: copy.title,
|
|
38
|
+
description: copy.description,
|
|
39
|
+
ui,
|
|
40
|
+
faq,
|
|
41
|
+
howTo,
|
|
42
|
+
bibliography,
|
|
43
|
+
schemas,
|
|
44
|
+
seo: [
|
|
45
|
+
{ type: 'summary', title: copy.title, items: copy.summary },
|
|
46
|
+
{ type: 'title', text: copy.seoTitle1, level: 2 },
|
|
47
|
+
{ type: 'paragraph', html: copy.seoIntro },
|
|
48
|
+
{ type: 'title', text: copy.seoTitle2, level: 2 },
|
|
49
|
+
{ type: 'paragraph', html: copy.seoMethod },
|
|
50
|
+
{ type: 'title', text: copy.seoTitle3, level: 2 },
|
|
51
|
+
{ type: 'paragraph', html: copy.seoSafety },
|
|
52
|
+
{ type: 'tip', title: copy.tipTitle, html: copy.tipText },
|
|
53
|
+
{ type: 'title', text: copy.scheduleTitle, level: 2 },
|
|
54
|
+
{ type: 'paragraph', html: [...copy.howTo.map((step) => `${step.name}: ${step.text}`), ...copy.faq.map((item) => `${item.question} ${item.answer}`), copy.methodText, copy.safetyText].join(' ') },
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContent, type MedicationCopy } from './content';
|
|
2
|
+
import { copy as en } from './en';
|
|
3
|
+
|
|
4
|
+
const copy: MedicationCopy = { ...en, slug: 'medikamentenplan-fuer-haustiere', title: 'Medikamentenplan für Haustiere', description: 'Machen Sie aus einer tierärztlich verordneten Behandlung einen klaren lokalen Zeitplan mit nächsten Gaben und Checkliste.', dateLocale: 'de', heroEyebrow: 'Verordnung einhalten. Alltag beruhigen.', heroHint: 'Tragen Sie die bereits erhaltenen Anweisungen ein. Dieser Planer berechnet und empfiehlt keine Dosis.', medicationNameLabel: 'Medikament', medicationNamePlaceholder: 'Zum Beispiel: verschriebenes Antibiotikum', startDateLabel: 'Datum der ersten Gabe', startTimeLabel: 'Uhrzeit der ersten Gabe', scheduleModeLabel: 'Wie ist der Rhythmus verordnet?', intervalMode: 'Alle paar Stunden', timesMode: 'Feste Uhrzeiten täglich', intervalHoursLabel: 'Stunden zwischen den Gaben', timesLabel: 'Tägliche Uhrzeiten', timesHint: '24-Stunden-Zeiten durch Kommas trennen, etwa 08:00, 16:00, 00:00.', durationLabel: 'Behandlungsdauer', durationUnit: 'Tage', instructionsLabel: 'Hinweise aus der Verordnung', instructionsPlaceholder: 'Futter, Verabreichung oder andere Hinweise der Tierarztpraxis', reset: 'Plan löschen', scheduleTitle: 'Ihr Gabeplan', scheduleSummary: 'Eine lokale Checkliste für eine verordnete Behandlung', nextDoseLabel: 'Nächste offene Gabe', noNextDose: 'Alle aufgeführten Gaben sind abgehakt', completedCount: '{done} von {total} abgehakt', markDone: 'Gabe als erledigt markieren', markUndone: 'Gabe wieder offen markieren', completed: 'Erledigt', upcoming: 'Kommend', due: 'Prüfen', emptySchedule: 'Füllen Sie die Felder aus, um den Gabeplan zu sehen.', invalidInput: 'Prüfen Sie Name, Datum, Uhrzeit, Dauer und Rhythmus.', localOnlyLabel: 'Läuft im Browser', safetyTitle: 'Die Verordnung genau befolgen', safetyText: 'Dies ist eine Erinnerung und Dokumentation. Es wählt kein Medikament, ändert keine Dosis und ersetzt keine tierärztliche Beratung. Bei verspäteter, vergessener oder erbrochener Gabe bitte die verordnende Praxis fragen.', methodTitle: 'So funktioniert es', methodText: 'Bei Intervallen beginnt der Plan mit Datum und Uhrzeit der ersten Gabe und addiert die gewählten Stunden. Tägliche Uhrzeiten werden an jedem Behandlungstag wiederholt. Die Angaben bleiben auf diesem Gerät, bis Sie sie löschen.', scheduleIllustration: 'Handgezeichneter Tagesplan neben einer ruhigen Katze und einem Hund', summary: ['Eine vorhandene Verordnung in einen lesbaren Plan übertragen.', 'Intervalle oder feste Uhrzeiten für jeden Tag wählen.', 'Gaben abhaken, ohne Tierdaten zu versenden.', 'Medikamentenname und Hinweise neben dem Ablauf behalten.'], seoTitle1: 'Eine praktische Erinnerung für eine bestehende tierärztliche Verordnung', seoIntro: 'Nach einem Praxisbesuch müssen Halter oft Etikett, Entlassungsbogen und mehrere Gaben zu Hause zusammenführen. Welche Behandlung nötig ist, entscheidet die Tierärztin oder der Tierarzt; dieser Planer ordnet nur die bereits vorliegenden Anweisungen. Name, erster lokaler Zeitpunkt, Rhythmus, Dauer und optionale Hinweise ergeben eine chronologische Liste für hektische Morgen, nächtliche Gaben oder die Übergabe zwischen Betreuungspersonen.', seoTitle2: 'Zwei Arten, den Rhythmus einzutragen', seoMethod: 'Wählen Sie ein Intervall, wenn die Verordnung etwa alle acht Stunden sagt. Der Plan läuft dabei über Mitternacht weiter. Wählen Sie tägliche Uhrzeiten, wenn morgens und abends oder konkrete Uhrzeiten genannt werden. Am ersten Tag wird keine frühere Zeit stillschweigend ergänzt. Die Dauer begrenzt nur den Planungszeitraum; sie interpretiert die Verordnung nicht. Das Ergebnis enthält Uhrzeiten, keine Medikamentenmengen.', seoTitle3: 'Eine Checkliste ist kein medizinischer Rat', seoSafety: 'Bewahren Sie Packung, Praxisunterlagen und Kontaktdaten auf. Die Seite kennt Tier, Konzentration, Verabreichungsweg, Wechselwirkungen und Reaktion nicht. Bei Unsicherheit zu einer verpassten Gabe immer die Praxis kontaktieren. Das Abhaken kann trotzdem Doppelgaben vermeiden und die nächste Aufgabe für mehrere Betreuungspersonen sichtbar machen. Der lokale Speicher ist nur eine Hilfe und jederzeit löschbar.', tipTitle: 'Vor dem Start', tipText: 'Übernehmen Sie Häufigkeit und Dauer aus der Verordnung, statt sie zu schätzen. Prüfen Sie den ersten erzeugten Tag noch einmal gegen das Etikett.', faq: [{ question: 'Berechnet der Planer die Medikamentenmenge?', answer: 'Nein. Er ordnet nur Uhrzeiten, die Sie aus einer bestehenden tierärztlichen Verordnung übernehmen. Mengen werden nicht berechnet, geändert oder empfohlen.' }, { question: 'Werden Gaben nach Mitternacht unterstützt?', answer: 'Ja. Intervalle laufen am lokalen Folgetag weiter; bei täglichen Uhrzeiten ist auch 00:00 möglich. Die Gruppierung folgt dem lokalen Browserdatum.' }, { question: 'Was tun bei einer verspäteten oder vergessenen Gabe?', answer: 'Fragen Sie die verordnende Tierarztpraxis. Die Liste kann den Stand dokumentieren, entscheidet aber nicht über Nachholen, Auslassen oder Verschieben.' }], howTo: [{ name: 'Verordnung übertragen', text: 'Name, ersten Zeitpunkt und Dauer genau nach der Anweisung der Tierarztpraxis eintragen.' }, { name: 'Rhythmus wählen', text: 'Ein Stundenintervall oder die verordneten täglichen Uhrzeiten auswählen.' }, { name: 'Plan prüfen', text: 'Ersten Tag und nächste offene Gabe kontrollieren, bevor der Plan als Haushaltserinnerung dient.' }, { name: 'Gaben abhaken', text: 'Nach der Gabe die Zeile antippen und die ursprünglichen Hinweise bereithalten.' }] };
|
|
5
|
+
|
|
6
|
+
export const content = createContent(copy);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createContent, type MedicationCopy } from './content';
|
|
2
|
+
|
|
3
|
+
export const copy: MedicationCopy = {
|
|
4
|
+
slug: 'pet-medication-schedule-planner', title: 'Pet Medication Schedule Planner', description: 'Turn a veterinarian-prescribed pet medication plan into a clear local schedule with upcoming doses and completion check-offs.', dateLocale: 'en', heroEyebrow: 'Follow the prescription. Calm the routine.', heroHint: 'Enter the instructions you already received to make a simple timetable for the next few days. This planner never calculates or recommends a dose.', medicationNameLabel: 'Medication name', medicationNamePlaceholder: 'For example: prescribed antibiotic', startDateLabel: 'First dose date', startTimeLabel: 'First dose time', scheduleModeLabel: 'How is the prescription timed?', intervalMode: 'Every few hours', timesMode: 'Set times each day', intervalHoursLabel: 'Hours between doses', timesLabel: 'Daily dose times', timesHint: 'Use 24-hour times separated by commas, such as 08:00, 16:00, 00:00.', durationLabel: 'Treatment length', durationUnit: 'days', instructionsLabel: 'Prescription notes', instructionsPlaceholder: 'Food, handling, or other instructions from your veterinarian', reset: 'Clear this schedule', scheduleTitle: 'Your dose board', scheduleSummary: 'A local checklist for one prescribed plan', nextDoseLabel: 'Next unchecked dose', noNextDose: 'All listed doses are checked', completedCount: '{done} of {total} checked', markDone: 'Mark dose as given', markUndone: 'Mark dose as not given', completed: 'Checked', upcoming: 'Upcoming', due: 'Review', emptySchedule: 'Complete the fields above to see the dose board.', invalidInput: 'Check the medication name, date, time, duration, and timing rule.', localOnlyLabel: 'Runs in your browser', safetyTitle: 'Use the prescription exactly as provided', safetyText: 'This is a reminder and record-keeping aid. Do not use it to choose a medicine, change a dose, or replace advice from your veterinarian. If a dose is late, missed, vomited, or disputed, contact the prescribing practice for instructions.', methodTitle: 'How it works', methodText: 'Interval plans begin at the first date and time, then add the selected number of hours until the treatment window ends. Daily-time plans repeat the times you enter on each treatment day. Everything stays on this device unless you choose to clear it.', scheduleIllustration: 'A hand-drawn daily timetable beside a calm cat and dog', summary: ['Build a short, readable timetable from an existing prescription.', 'Choose fixed daily times or a simple interval between doses.', 'Mark each dose as checked without sending pet information anywhere.', 'Keep the medicine name and notes visible beside the routine.'], seoTitle1: 'A practical reminder for an existing veterinary plan', seoIntro: 'Caregivers often leave a clinic with a label, a verbal instruction, or several medicines to coordinate at home. The hard part is not deciding what the animal should receive: that decision belongs to the prescribing veterinarian. The hard part is turning the instructions already in hand into a routine that is easy to scan during a busy morning, overnight wake-up, or multi-day recovery. This planner accepts the medicine name, the first local date and time, a timing pattern, the number of treatment days, and optional notes. It creates a chronological checklist that can be reviewed on the same device where it was entered.', seoTitle2: 'Two ways to describe the timing', seoMethod: 'Use an interval when the label says something like every eight hours. The planner starts from the first dose and adds that interval, including doses that cross midnight. Use daily times when the prescription gives moments such as morning and evening or a set of clock times. The first day respects the first-dose time, so an earlier daily time is not silently added. The treatment length is a bounded planning window; it is not an interpretation of the medicine label. The result is a schedule of times, not a dose calculator.', seoTitle3: 'A checklist is not medical advice', seoSafety: 'Keep the original label, discharge sheet, and veterinary contact details available. This page does not know the animal, medicine, concentration, route, interactions, or clinical response. Never use a missed-dose decision from this page as a substitute for professional advice. Record keeping can still help: checking a dose reduces uncertainty between caregivers, and the visible next unchecked dose makes handovers easier. The local storage feature is only a convenience and can be cleared at any time.', tipTitle: 'Before you begin', tipText: 'Copy the timing and duration from the prescription rather than estimating them. Add practical notes only when they came from the veterinary team, and verify the first generated day against the label before relying on the checklist.', faq: [{ question: 'Does this planner calculate the amount of medicine?', answer: 'No. It only lays out a timing plan that you enter from an existing veterinary prescription. It does not calculate, adjust, or recommend an amount.' }, { question: 'Can it handle doses that cross midnight?', answer: 'Yes. An interval schedule continues across local calendar days, and a daily-time schedule can include a time such as 00:00. The list is grouped by the local date shown in your browser.' }, { question: 'What should I do if a dose is late or missed?', answer: 'Contact the prescribing veterinarian or practice for instructions. The checklist can record what happened, but it cannot decide whether to give, skip, or move a dose.' }], howTo: [{ name: 'Copy the prescription details', text: 'Enter the medicine name, first dose date and time, and the treatment length exactly as provided by the veterinary team.' }, { name: 'Choose the timing pattern', text: 'Select an interval in hours or enter the daily clock times named by the prescription.' }, { name: 'Read the generated board', text: 'Review the first day and the next unchecked dose before using the list as a household reminder.' }, { name: 'Check off each dose', text: 'Tap a row after the dose has been given and keep the original instructions nearby for any uncertainty.' }],
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const content = createContent(copy);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContent, type MedicationCopy } from './content';
|
|
2
|
+
import { copy as en } from './en';
|
|
3
|
+
|
|
4
|
+
const copy: MedicationCopy = { ...en, slug: 'planificador-horario-medicacion-mascota', title: 'Planificador de medicación para mascotas', description: 'Convierte una pauta veterinaria prescrita en un horario local claro, con próximas tomas y casillas de seguimiento.', dateLocale: 'es', heroEyebrow: 'Sigue la receta. Ordena la rutina.', heroHint: 'Introduce las instrucciones que ya te dieron para crear un horario sencillo. Esta herramienta no calcula ni recomienda dosis.', medicationNameLabel: 'Nombre del medicamento', medicationNamePlaceholder: 'Por ejemplo: antibiótico prescrito', startDateLabel: 'Fecha de la primera toma', startTimeLabel: 'Hora de la primera toma', scheduleModeLabel: '¿Cómo está indicada la pauta?', intervalMode: 'Cada ciertas horas', timesMode: 'Horas fijas cada día', intervalHoursLabel: 'Horas entre tomas', timesLabel: 'Horas diarias', timesHint: 'Usa horas de 24 h separadas por comas, por ejemplo 08:00, 16:00, 00:00.', durationLabel: 'Duración del tratamiento', durationUnit: 'días', instructionsLabel: 'Notas de la receta', instructionsPlaceholder: 'Comida, forma de administración u otras indicaciones veterinarias', reset: 'Borrar este horario', scheduleTitle: 'Panel de tomas', scheduleSummary: 'Una lista local para una pauta prescrita', nextDoseLabel: 'Siguiente toma sin marcar', noNextDose: 'Todas las tomas están marcadas', completedCount: '{done} de {total} marcadas', markDone: 'Marcar toma administrada', markUndone: 'Marcar toma como pendiente', completed: 'Hecha', upcoming: 'Próxima', due: 'Revisar', emptySchedule: 'Completa los campos para ver el panel de tomas.', invalidInput: 'Revisa el nombre, la fecha, la hora, la duración y la regla de horario.', localOnlyLabel: 'Funciona en tu navegador', safetyTitle: 'Usa exactamente la pauta prescrita', safetyText: 'Es una ayuda para recordar y registrar. No elige medicamentos, cambia dosis ni sustituye al veterinario. Si una toma se retrasa, se olvida o se vomita, contacta con la clínica que la prescribió.', methodTitle: 'Cómo funciona', methodText: 'Las pautas por intervalo parten de la primera fecha y hora y suman las horas elegidas hasta terminar el periodo. Las pautas por horas diarias repiten las horas introducidas en cada día. Todo queda en este dispositivo salvo que decidas borrarlo.', scheduleIllustration: 'Calendario dibujado a mano junto a un gato y un perro tranquilos', summary: ['Crea un horario legible a partir de una receta existente.', 'Elige intervalos de horas o momentos fijos del día.', 'Marca cada toma sin enviar datos de tu mascota.', 'Mantén visibles el nombre y las notas de la pauta.'], seoTitle1: 'Un recordatorio práctico para una pauta veterinaria existente', seoIntro: 'Después de una consulta, la dificultad suele ser convertir una etiqueta o varias instrucciones en una rutina doméstica que se entienda de un vistazo. Esta herramienta no decide qué debe recibir el animal: esa decisión corresponde al veterinario que lo ha examinado. Solo pide el nombre, la primera fecha y hora, la forma de repetir la pauta, los días de tratamiento y las notas que quieras conservar. El resultado es una lista cronológica para consultar durante la mañana, una noche con varias personas cuidadoras o una recuperación de varios días.', seoTitle2: 'Dos formas de describir las horas', seoMethod: 'Elige un intervalo cuando la etiqueta indique algo como cada ocho horas. La siguiente toma se calcula sumando ese intervalo, también al pasar de un día al siguiente. Elige horas diarias cuando la pauta diga mañana y noche o enumere horas concretas. El primer día respeta la primera toma y no añade en silencio una hora anterior. La duración limita el periodo de planificación; no interpreta ni modifica la receta. El resultado son horas, no cantidades de medicamento.', seoTitle3: 'Una lista de control no es consejo médico', seoSafety: 'Conserva cerca la etiqueta original, el informe de la clínica y su teléfono. La página no conoce el animal, la concentración, la vía, las interacciones ni su respuesta. Ante una toma olvidada o dudosa, pregunta al equipo veterinario antes de decidir. Marcar las tomas sí puede reducir confusiones entre cuidadores y hacer más clara la siguiente acción. El almacenamiento local es solo una comodidad y puede borrarse en cualquier momento.', tipTitle: 'Antes de empezar', tipText: 'Copia de la receta la frecuencia y la duración, sin estimarlas. Añade solo notas procedentes del equipo veterinario y comprueba el primer día generado con la etiqueta antes de confiar en la lista.', faq: [{ question: '¿La herramienta calcula la cantidad de medicamento?', answer: 'No. Solo organiza horarios que introduces a partir de una receta veterinaria existente. No calcula, ajusta ni recomienda cantidades.' }, { question: '¿Puede mostrar tomas que pasan de medianoche?', answer: 'Sí. Un intervalo continúa en el día local siguiente y las horas diarias pueden incluir 00:00. La lista se agrupa según la fecha local del navegador.' }, { question: '¿Qué hago si una toma se retrasa o se olvida?', answer: 'Contacta con el veterinario o la clínica que indicó la pauta. La lista puede registrar lo ocurrido, pero no decide si debes dar, saltar o mover una toma.' }], howTo: [{ name: 'Copia los datos de la receta', text: 'Introduce el nombre, la primera fecha y hora y la duración exactamente como los indicó el equipo veterinario.' }, { name: 'Elige el patrón horario', text: 'Selecciona un intervalo en horas o escribe las horas diarias de la receta.' }, { name: 'Revisa el panel', text: 'Comprueba el primer día y la siguiente toma sin marcar antes de usarlo como recordatorio doméstico.' }, { name: 'Marca cada toma', text: 'Toca una fila después de administrar la toma y conserva cerca las instrucciones originales.' }] };
|
|
5
|
+
|
|
6
|
+
export const content = createContent(copy);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContent, type MedicationCopy } from './content';
|
|
2
|
+
import { copy as en } from './en';
|
|
3
|
+
|
|
4
|
+
const copy: MedicationCopy = { ...en, slug: 'planning-traitement-animal', title: 'Planning des médicaments pour animaux', description: 'Transformez une prescription vétérinaire en calendrier local lisible avec prochaines prises et cases de suivi.', dateLocale: 'fr', heroEyebrow: 'Suivez l ordonnance. Apaisez la routine.', heroHint: 'Saisissez les consignes déjà reçues pour créer un rappel simple. Ce planning ne calcule ni ne conseille une dose.', medicationNameLabel: 'Nom du médicament', medicationNamePlaceholder: 'Par exemple: antibiotique prescrit', startDateLabel: 'Date de la première prise', startTimeLabel: 'Heure de la première prise', scheduleModeLabel: 'Comment la prescription est-elle rythmée ?', intervalMode: 'Toutes les quelques heures', timesMode: 'Heures fixes chaque jour', intervalHoursLabel: 'Heures entre les prises', timesLabel: 'Heures quotidiennes', timesHint: 'Utilisez des heures sur 24 h séparées par des virgules: 08:00, 16:00, 00:00.', durationLabel: 'Durée du traitement', durationUnit: 'jours', instructionsLabel: 'Notes de prescription', instructionsPlaceholder: 'Alimentation, administration ou autre consigne du vétérinaire', reset: 'Effacer ce planning', scheduleTitle: 'Tableau des prises', scheduleSummary: 'Une checklist locale pour un traitement prescrit', nextDoseLabel: 'Prochaine prise non cochée', noNextDose: 'Toutes les prises sont cochées', completedCount: '{done} sur {total} cochées', markDone: 'Marquer la prise donnée', markUndone: 'Marquer la prise non donnée', completed: 'Faite', upcoming: 'À venir', due: 'À vérifier', emptySchedule: 'Remplissez les champs pour afficher le tableau.', invalidInput: 'Vérifiez le nom, la date, l heure, la durée et le rythme.', localOnlyLabel: 'Fonctionne dans votre navigateur', safetyTitle: 'Respectez la prescription telle quelle', safetyText: 'Cet outil sert de rappel et de suivi. Il ne choisit pas un médicament, ne modifie pas une dose et ne remplace pas le vétérinaire. En cas de retard, d oubli ou de vomissement, appelez la clinique prescriptrice.', methodTitle: 'Fonctionnement', methodText: 'Un intervalle part de la première date et heure puis ajoute le nombre d heures choisi. Les heures quotidiennes se répètent chaque jour de traitement. Les informations restent sur cet appareil jusqu à leur effacement.', scheduleIllustration: 'Calendrier dessiné à la main près d un chat et d un chien calmes', summary: ['Transformez une prescription existante en tableau lisible.', 'Choisissez un intervalle ou des heures fixes.', 'Cochez chaque prise sans transmettre de données.', 'Gardez le nom et les notes visibles pendant la routine.'], seoTitle1: 'Un rappel pratique pour un traitement déjà prescrit', seoIntro: 'Après une consultation, il faut souvent coordonner une étiquette, une feuille de sortie et plusieurs prises dans la maison. Le choix du médicament appartient au vétérinaire ; la difficulté consiste à rendre les instructions déjà reçues faciles à relire. Ce planning demande seulement le nom, le premier moment local, le rythme, la durée et des notes facultatives. Il transforme ces informations en liste chronologique pour les matins chargés, les nuits ou les relais entre proches.', seoTitle2: 'Décrire les horaires de deux façons', seoMethod: 'Choisissez un intervalle si l étiquette dit par exemple toutes les huit heures. Le calcul poursuit naturellement la liste après minuit. Choisissez des heures quotidiennes si la prescription indique matin et soir ou des heures précises. Le premier jour respecte la première prise et n ajoute pas une heure antérieure. La durée est une fenêtre de planification, jamais une interprétation de la prescription. Le résultat affiche des horaires, pas des quantités.', seoTitle3: 'Une checklist ne remplace pas un avis médical', seoSafety: 'Gardez la boîte, la feuille de la clinique et son numéro à portée de main. La page ne connaît ni l animal, ni la concentration, ni les interactions, ni sa réaction. Pour une prise manquée ou incertaine, demandez conseil à l équipe vétérinaire. Cocher les prises réduit néanmoins les doublons entre aidants et rend le prochain geste évident. Le stockage local est optionnel et peut être supprimé à tout moment.', tipTitle: 'Avant de commencer', tipText: 'Recopiez la fréquence et la durée de la prescription, sans les deviner. Ajoutez uniquement les notes données par le vétérinaire et vérifiez le premier jour affiché avec l étiquette.', faq: [{ question: 'Le planning calcule-t-il la quantité ?', answer: 'Non. Il organise uniquement des horaires saisis depuis une prescription existante. Il ne calcule, ne modifie et ne recommande aucune quantité.' }, { question: 'Les prises après minuit sont-elles gérées ?', answer: 'Oui. Un intervalle continue sur le jour local suivant et les heures quotidiennes peuvent contenir 00:00. Les prises sont groupées selon la date locale du navigateur.' }, { question: 'Que faire si une prise est en retard ou oubliée ?', answer: 'Appelez le vétérinaire ou la clinique prescriptrice. La checklist peut noter la situation, mais elle ne décide pas de donner, sauter ou déplacer une prise.' }], howTo: [{ name: 'Recopier la prescription', text: 'Saisissez le nom, la première date et heure et la durée exactement comme indiqués par l équipe vétérinaire.' }, { name: 'Choisir le rythme', text: 'Sélectionnez un intervalle en heures ou saisissez les heures quotidiennes prescrites.' }, { name: 'Relire le tableau', text: 'Vérifiez le premier jour et la prochaine prise non cochée avant de l utiliser comme rappel.' }, { name: 'Cocher chaque prise', text: 'Touchez une ligne après administration et gardez les consignes originales à proximité.' }] };
|
|
5
|
+
|
|
6
|
+
export const content = createContent(copy);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContent, type MedicationCopy } from './content';
|
|
2
|
+
import { copy as en } from './en';
|
|
3
|
+
|
|
4
|
+
const copy: MedicationCopy = { ...en, slug: 'jadwal-obat-hewan', title: 'Jadwal Obat Hewan Peliharaan', description: 'Ubah resep dokter hewan menjadi jadwal lokal yang jelas dengan dosis berikutnya dan daftar centang.', dateLocale: 'id-ID', heroEyebrow: 'Ikuti resep. Tenangkan rutinitas.', heroHint: 'Masukkan petunjuk yang sudah Anda terima. Alat ini tidak menghitung atau menyarankan dosis.', medicationNameLabel: 'Nama obat', medicationNamePlaceholder: 'Contoh: antibiotik resep', startDateLabel: 'Tanggal pemberian pertama', startTimeLabel: 'Waktu pemberian pertama', scheduleModeLabel: 'Bagaimana jadwalnya ditulis?', intervalMode: 'Setiap beberapa jam', timesMode: 'Waktu tetap setiap hari', intervalHoursLabel: 'Jam di antara pemberian', timesLabel: 'Waktu harian', timesHint: 'Gunakan format 24 jam dan pisahkan dengan koma: 08:00, 16:00, 00:00.', durationLabel: 'Lama pengobatan', durationUnit: 'hari', instructionsLabel: 'Catatan resep', instructionsPlaceholder: 'Makanan, cara pemberian, atau petunjuk dokter hewan lain', reset: 'Hapus jadwal', scheduleTitle: 'Papan pemberian', scheduleSummary: 'Daftar lokal untuk satu resep', nextDoseLabel: 'Pemberian berikutnya yang belum dicentang', noNextDose: 'Semua pemberian yang tampil sudah dicentang', completedCount: '{done} dari {total} dicentang', markDone: 'Tandai sudah diberikan', markUndone: 'Tandai belum diberikan', completed: 'Selesai', upcoming: 'Berikutnya', due: 'Periksa', emptySchedule: 'Lengkapi kolom untuk melihat jadwal.', invalidInput: 'Periksa nama, tanggal, waktu, lama, dan aturan jadwal.', localOnlyLabel: 'Berjalan di browser', safetyTitle: 'Ikuti resep apa adanya', safetyText: 'Ini hanya alat pengingat dan pencatatan. Alat ini tidak memilih obat, mengubah dosis, atau menggantikan dokter hewan. Hubungi klinik jika pemberian terlambat, terlupa, atau dimuntahkan.', methodTitle: 'Cara kerja', methodText: 'Jadwal interval dimulai dari tanggal dan waktu pertama, lalu menambahkan jam yang dipilih. Waktu harian diulang pada setiap hari pengobatan. Data tetap di perangkat ini sampai Anda menghapusnya.', scheduleIllustration: 'Jadwal harian bergambar tangan di samping kucing dan anjing yang tenang', summary: ['Jadikan resep yang ada sebagai jadwal yang mudah dibaca.', 'Pilih interval atau waktu tetap setiap hari.', 'Centang pemberian tanpa mengirim data hewan.', 'Simpan nama obat dan catatan di dekat rutinitas.'], seoTitle1: 'Pengingat praktis untuk resep dokter hewan yang sudah ada', seoIntro: 'Setelah kunjungan klinik, pemilik sering harus menyatukan label, lembar pulang, dan beberapa pemberian di rumah. Keputusan pengobatan dibuat oleh dokter hewan; alat ini hanya merapikan instruksi yang sudah diterima. Nama obat, waktu pertama, pola, lama hari, dan catatan membentuk daftar kronologis untuk pagi yang sibuk, malam, atau pergantian pengasuh.', seoTitle2: 'Dua cara menuliskan waktu', seoMethod: 'Pilih interval jika label mengatakan, misalnya, setiap delapan jam. Daftar akan berlanjut melewati tengah malam. Pilih waktu harian jika resep menyebut pagi dan malam atau jam tertentu. Hari pertama mengikuti pemberian pertama dan tidak menambahkan waktu lebih awal. Lama pengobatan adalah jendela perencanaan, bukan tafsiran resep. Hasilnya berisi waktu, bukan jumlah obat.', seoTitle3: 'Daftar centang bukan nasihat medis', seoSafety: 'Simpan kemasan, dokumen klinik, dan nomor teleponnya. Halaman ini tidak mengetahui hewan, konsentrasi, cara pemberian, interaksi, atau responsnya. Tanyakan klinik sebelum mengambil keputusan tentang pemberian yang terlewat. Pencatatan tetap dapat mencegah pemberian ganda dan memperjelas tugas berikutnya. Penyimpanan lokal dapat dihapus kapan saja.', tipTitle: 'Sebelum mulai', tipText: 'Salin frekuensi dan lama pengobatan dari resep, jangan menebak. Bandingkan hari pertama yang dibuat dengan label.', faq: [{ question: 'Apakah alat ini menghitung jumlah obat?', answer: 'Tidak. Alat ini hanya menyusun waktu yang Anda masukkan dari resep yang ada dan tidak menghitung, mengubah, atau menyarankan jumlah.' }, { question: 'Apakah waktu setelah tengah malam didukung?', answer: 'Ya. Interval berlanjut ke hari lokal berikutnya dan waktu harian dapat berisi 00:00.' }, { question: 'Apa yang dilakukan jika pemberian terlambat atau terlewat?', answer: 'Hubungi dokter hewan atau klinik yang memberi resep. Daftar mencatat keadaan, tetapi tidak menentukan apakah harus mengejar, melewati, atau memindahkan pemberian.' }], howTo: [{ name: 'Salin resep', text: 'Masukkan nama, waktu pertama, dan lama pengobatan sesuai petunjuk dokter hewan.' }, { name: 'Pilih pola waktu', text: 'Gunakan interval jam atau waktu harian dari resep.' }, { name: 'Periksa papan', text: 'Pastikan hari pertama dan pemberian berikutnya yang belum dicentang.' }, { name: 'Centang setiap pemberian', text: 'Ketuk baris setelah obat diberikan dan simpan petunjuk asli di dekat Anda.' }] };
|
|
5
|
+
|
|
6
|
+
export const content = createContent(copy);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContent, type MedicationCopy } from './content';
|
|
2
|
+
import { copy as en } from './en';
|
|
3
|
+
|
|
4
|
+
const copy: MedicationCopy = { ...en, slug: 'agenda-medicinali-animali', title: 'Agenda dei medicinali per animali', description: 'Trasforma una terapia prescritta dal veterinario in un calendario locale chiaro, con prossime somministrazioni e spunte.', dateLocale: 'it', heroEyebrow: 'Segui la prescrizione. Semplifica la routine.', heroHint: 'Inserisci le indicazioni già ricevute per creare un promemoria breve. Questo strumento non calcola né consiglia dosi.', medicationNameLabel: 'Nome del medicinale', medicationNamePlaceholder: 'Per esempio: antibiotico prescritto', startDateLabel: 'Data della prima somministrazione', startTimeLabel: 'Ora della prima somministrazione', scheduleModeLabel: 'Come è indicata la frequenza?', intervalMode: 'Ogni alcune ore', timesMode: 'Orari fissi ogni giorno', intervalHoursLabel: 'Ore tra le somministrazioni', timesLabel: 'Orari giornalieri', timesHint: 'Usa orari sulle 24 ore separati da virgole: 08:00, 16:00, 00:00.', durationLabel: 'Durata della terapia', durationUnit: 'giorni', instructionsLabel: 'Note della prescrizione', instructionsPlaceholder: 'Cibo, modalità o altre istruzioni del veterinario', reset: 'Cancella questo calendario', scheduleTitle: 'Tabella delle somministrazioni', scheduleSummary: 'Una checklist locale per una terapia prescritta', nextDoseLabel: 'Prossima somministrazione non spuntata', noNextDose: 'Tutte le somministrazioni sono spuntate', completedCount: '{done} di {total} spuntate', markDone: 'Segna come somministrata', markUndone: 'Segna come non somministrata', completed: 'Fatta', upcoming: 'In arrivo', due: 'Da controllare', emptySchedule: 'Completa i campi per vedere la tabella.', invalidInput: 'Controlla nome, data, ora, durata e frequenza.', localOnlyLabel: 'Funziona nel browser', safetyTitle: 'Segui la prescrizione senza modificarla', safetyText: 'È un aiuto per ricordare e registrare. Non sceglie medicinali, non cambia dosi e non sostituisce il veterinario. Per una somministrazione in ritardo, dimenticata o vomitata, contatta la clinica.', methodTitle: 'Come funziona', methodText: 'Con un intervallo il calendario parte dal primo momento e aggiunge le ore indicate. Con orari giornalieri ripete gli orari inseriti per ogni giorno. I dati restano su questo dispositivo finché non li cancelli.', scheduleIllustration: 'Calendario disegnato a mano accanto a un gatto e un cane tranquilli', summary: ['Rendi leggibile una prescrizione già disponibile.', 'Scegli intervalli oppure orari fissi della giornata.', 'Spunta le somministrazioni senza inviare dati.', 'Tieni nome e note accanto alla routine.'], seoTitle1: 'Un promemoria pratico per una terapia già prescritta', seoIntro: 'Dopo la visita, il compito difficile è coordinare etichetta, foglio della clinica e più somministrazioni in casa. La scelta del farmaco spetta al veterinario: questa agenda organizza soltanto le istruzioni che hai già ricevuto. Inserisci nome, primo momento locale, frequenza, durata e note facoltative per ottenere una lista cronologica facile da leggere al mattino, di notte o durante il passaggio di consegne.', seoTitle2: 'Due modi per indicare gli orari', seoMethod: 'Scegli un intervallo se la prescrizione dice, per esempio, ogni otto ore. La sequenza continua correttamente oltre la mezzanotte. Scegli gli orari giornalieri se sono indicati mattina e sera o orari precisi. Nel primo giorno non viene aggiunto in automatico un orario precedente alla prima somministrazione. La durata è una finestra di pianificazione, non un interpretazione della ricetta. Il risultato mostra orari, non quantità.', seoTitle3: 'Una checklist non è un consiglio medico', seoSafety: 'Conserva confezione, foglio della clinica e recapiti. La pagina non conosce animale, concentrazione, via di somministrazione, interazioni o risposta. Per una dose dimenticata chiedi alla clinica prima di decidere. Spuntare le righe può comunque evitare doppie somministrazioni e rendere chiara la prossima azione. Il salvataggio locale è una comodità che puoi cancellare.', tipTitle: 'Prima di iniziare', tipText: 'Copia frequenza e durata dalla prescrizione senza stimarle. Controlla il primo giorno generato insieme all etichetta.', faq: [{ question: 'Calcola la quantità del medicinale?', answer: 'No. Organizza solo gli orari inseriti da una prescrizione esistente e non calcola, modifica o consiglia quantità.' }, { question: 'Gestisce gli orari dopo mezzanotte?', answer: 'Sì. Gli intervalli passano al giorno locale successivo e gli orari giornalieri possono includere 00:00.' }, { question: 'Cosa faccio se una somministrazione è in ritardo?', answer: 'Contatta il veterinario o la clinica che ha prescritto la terapia. La checklist registra lo stato, ma non decide se recuperare, saltare o spostare.' }], howTo: [{ name: 'Trascrivi la prescrizione', text: 'Inserisci nome, primo momento e durata esattamente come indicato dal veterinario.' }, { name: 'Scegli la frequenza', text: 'Usa un intervallo in ore oppure gli orari giornalieri prescritti.' }, { name: 'Controlla il quadro', text: 'Verifica il primo giorno e la prossima riga non spuntata.' }, { name: 'Spunta le righe', text: 'Tocca una riga dopo la somministrazione e conserva le istruzioni originali.' }] };
|
|
5
|
+
|
|
6
|
+
export const content = createContent(copy);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContent, type MedicationCopy } from './content';
|
|
2
|
+
import { copy as en } from './en';
|
|
3
|
+
|
|
4
|
+
const copy: MedicationCopy = { ...en, slug: 'pet-medication-schedule-planner-ja', title: 'ペットの投薬スケジュール作成', description: '獣医師から処方された投薬内容を、次の投薬と確認欄のある見やすい端末内スケジュールに整理します。', dateLocale: 'ja-JP', heroEyebrow: '処方どおりに。毎日の負担を軽く。', heroHint: '受け取った指示を入力して、数日分の予定表を作ります。このツールは投薬量の計算や提案をしません。', medicationNameLabel: '薬の名前', medicationNamePlaceholder: '例:処方された抗生剤', startDateLabel: '最初に飲ませる日', startTimeLabel: '最初に飲ませる時刻', scheduleModeLabel: '処方の時間指定方法', intervalMode: '数時間ごと', timesMode: '毎日の固定時刻', intervalHoursLabel: '投薬間隔(時間)', timesLabel: '毎日の時刻', timesHint: '24時間表記でカンマ区切りにします。例:08:00, 16:00, 00:00', durationLabel: '治療期間', durationUnit: '日', instructionsLabel: '処方時のメモ', instructionsPlaceholder: '食事、与え方、その他の獣医師の指示', reset: '予定を消去', scheduleTitle: '投薬ボード', scheduleSummary: '処方された予定を端末内で確認するリスト', nextDoseLabel: '次の未確認の投薬', noNextDose: '表示された投薬はすべて確認済みです', completedCount: '{done} / {total} 件を確認', markDone: '投薬済みにする', markUndone: '未投薬に戻す', completed: '確認済み', upcoming: '次回', due: '確認', emptySchedule: '項目を入力すると投薬ボードが表示されます。', invalidInput: '薬名、日付、時刻、期間、時間設定を確認してください。', localOnlyLabel: 'ブラウザ内で動作', safetyTitle: '処方内容をそのまま使う', safetyText: 'これは記録とリマインダーの補助ツールです。薬や量を選んだり変更したり、獣医師の助言に代わったりしません。遅れ、飲み忘れ、吐き戻しがあれば処方した動物病院に相談してください。', methodTitle: '仕組み', methodText: '間隔指定では最初の日付と時刻から指定時間を順に加えます。毎日の時刻指定では入力した時刻を治療期間の各日に繰り返します。消去するまで情報はこの端末に保存されます。', scheduleIllustration: '穏やかな猫と犬のそばに描かれた一日の投薬予定表', summary: ['手元の処方内容を読みやすい予定表に整理します。', '時間間隔または毎日の固定時刻を選べます。', 'ペットの情報を送信せず、投薬を確認できます。', '薬名と注意事項を予定表の横に残せます。'], seoTitle1: 'すでにある獣医師の処方を確認しやすくする予定表', seoIntro: '動物病院から帰った後は、薬のラベルや説明書、複数回の投薬を家庭で管理する必要があります。何を使うかを決めるのは獣医師です。このツールは、すでに受け取った内容を日付順のチェックリストに整理します。薬名、最初の日時、間隔または時刻、日数、メモを入力すれば、朝の忙しい時間や夜間、家族間の引き継ぎでも確認しやすくなります。', seoTitle2: '時間の入力方法は二つ', seoMethod: '「8時間ごと」のような指示なら間隔を選びます。日付が変わっても次の予定を続けて表示します。「朝と夜」や具体的な時刻なら毎日の時刻を選びます。最初の日は最初の投薬時刻より前の予定を自動で追加しません。期間は表示範囲を決めるだけで、処方内容を解釈するものではありません。表示するのは時刻であり、薬の量ではありません。', seoTitle3: 'チェックリストは医療助言ではありません', seoSafety: '薬の箱、病院の説明書、連絡先を手元に置いてください。このページは動物の状態、濃度、投与方法、相互作用、反応を判断できません。飲み忘れなどで迷ったときは、決める前に病院へ連絡してください。確認欄は二重投与を防ぎ、次の担当者に状況を伝える助けになります。端末内の保存はいつでも消去できます。', tipTitle: '始める前に', tipText: '頻度と期間は処方からそのまま写し、推測しないでください。作成された最初の日をラベルと照合してください。', faq: [{ question: '投薬量を計算できますか?', answer: 'いいえ。既存の処方から入力した時刻を整理するだけで、量を計算、変更、提案することはありません。' }, { question: '日付をまたぐ投薬に対応していますか?', answer: 'はい。間隔指定は地域の日付をまたいで続き、毎日の時刻には00:00も入力できます。' }, { question: '投薬が遅れたり抜けたりしたらどうしますか?', answer: '処方した獣医師または動物病院に相談してください。このリストは状況を記録しますが、追加、スキップ、変更を判断しません。' }], howTo: [{ name: '処方内容を入力する', text: '獣医師の指示どおりに薬名、最初の日時、期間を入力します。' }, { name: '時間設定を選ぶ', text: '時間間隔か、処方にある毎日の時刻を選びます。' }, { name: '最初の日を確認する', text: '最初の日と次の未確認の投薬を、使い始める前に確認します。' }, { name: '投薬を確認する', text: '与えた後に行をタップし、元の説明書も近くに置いてください。' }] };
|
|
5
|
+
|
|
6
|
+
export const content = createContent(copy);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContent, type MedicationCopy } from './content';
|
|
2
|
+
import { copy as en } from './en';
|
|
3
|
+
|
|
4
|
+
const copy: MedicationCopy = { ...en, slug: 'pet-medication-schedule-planner-ko', title: '반려동물 투약 일정표', description: '수의사가 처방한 투약 계획을 다음 투약 시간과 확인 표시가 있는 보기 쉬운 일정으로 정리합니다.', dateLocale: 'ko-KR', heroEyebrow: '처방을 따르고, 돌봄을 단순하게.', heroHint: '이미 받은 지시사항을 입력하세요. 이 도구는 용량을 계산하거나 추천하지 않습니다.', medicationNameLabel: '약 이름', medicationNamePlaceholder: '예: 처방받은 항생제', startDateLabel: '첫 투약 날짜', startTimeLabel: '첫 투약 시간', scheduleModeLabel: '처방 시간은 어떻게 정해져 있나요?', intervalMode: '몇 시간마다', timesMode: '매일 정해진 시간', intervalHoursLabel: '투약 사이 시간', timesLabel: '매일의 시간', timesHint: '24시간 형식으로 쉼표를 사용하세요. 예: 08:00, 16:00, 00:00', durationLabel: '치료 기간', durationUnit: '일', instructionsLabel: '처방 메모', instructionsPlaceholder: '식사, 투여 방법 또는 수의사의 다른 지시', reset: '일정 지우기', scheduleTitle: '투약 보드', scheduleSummary: '한 가지 처방을 위한 기기 내 체크리스트', nextDoseLabel: '아직 확인하지 않은 다음 투약', noNextDose: '표시된 투약을 모두 확인했습니다', completedCount: '{total}개 중 {done}개 확인', markDone: '투약 완료로 표시', markUndone: '미완료로 되돌리기', completed: '완료', upcoming: '예정', due: '확인', emptySchedule: '항목을 입력하면 투약 보드가 나타납니다.', invalidInput: '약 이름, 날짜, 시간, 기간과 시간 규칙을 확인하세요.', localOnlyLabel: '브라우저에서 실행', safetyTitle: '처방 내용을 그대로 따르세요', safetyText: '이 도구는 기록과 알림을 돕습니다. 약을 선택하거나 용량을 바꾸지 않으며 수의사의 조언을 대신하지 않습니다. 늦었거나 잊었거나 토한 투약은 처방한 병원에 문의하세요.', methodTitle: '작동 방식', methodText: '간격 방식은 첫 날짜와 시간에서 시작해 선택한 시간을 더합니다. 매일의 시간 방식은 치료 기간 동안 입력한 시간을 반복합니다. 삭제하기 전까지 정보는 이 기기에 남습니다.', scheduleIllustration: '차분한 고양이와 강아지 옆에 그려진 하루 투약표', summary: ['기존 처방을 읽기 쉬운 일정으로 정리합니다.', '시간 간격이나 매일의 고정 시간을 선택합니다.', '반려동물 정보를 보내지 않고 투약을 확인합니다.', '약 이름과 메모를 일정 옆에 보관합니다.'], seoTitle1: '이미 받은 수의사 처방을 위한 실용적인 알림', seoIntro: '동물병원에 다녀온 뒤에는 약 라벨, 안내문, 여러 번의 투약을 집에서 맞춰야 합니다. 어떤 치료를 할지는 수의사가 결정하며, 이 도구는 이미 받은 지시만 순서대로 정리합니다. 약 이름, 첫 시간, 반복 방식, 기간과 메모를 입력하면 바쁜 아침, 밤중, 보호자 교대 때 확인할 수 있는 시간순 목록이 만들어집니다.', seoTitle2: '시간을 입력하는 두 가지 방법', seoMethod: '라벨에 8시간마다처럼 적혀 있으면 간격 방식을 사용합니다. 자정이 지나도 다음 날짜로 이어집니다. 아침과 저녁 또는 특정 시각이 적혀 있으면 매일의 시간을 사용합니다. 첫날에는 첫 투약보다 이른 시간이 자동으로 추가되지 않습니다. 기간은 계획 범위를 정할 뿐 처방을 해석하지 않으며, 결과에는 약의 양이 아니라 시간이 표시됩니다.', seoTitle3: '체크리스트는 의료 조언이 아닙니다', seoSafety: '약 포장, 병원 안내문과 연락처를 가까이 두세요. 이 페이지는 동물의 상태, 농도, 투여 경로, 상호작용이나 반응을 알 수 없습니다. 투약을 놓쳤다면 먼저 병원에 문의하세요. 확인 표시는 중복 투약을 줄이고 다음 담당자에게 상황을 알려주는 데 도움이 됩니다. 기기 내 저장 내용은 언제든 삭제할 수 있습니다.', tipTitle: '시작하기 전에', tipText: '횟수와 기간은 처방에서 그대로 옮기고 추측하지 마세요. 만들어진 첫날을 약 라벨과 대조하세요.', faq: [{ question: '투약량을 계산하나요?', answer: '아니요. 기존 처방에서 입력한 시간을 정리할 뿐이며 양을 계산, 변경 또는 추천하지 않습니다.' }, { question: '자정을 넘기는 투약도 표시하나요?', answer: '네. 간격 방식은 다음 지역 날짜로 이어지고, 매일의 시간에는 00:00을 넣을 수 있습니다.' }, { question: '투약이 늦거나 빠졌다면 어떻게 하나요?', answer: '처방한 수의사나 병원에 문의하세요. 목록은 상태를 기록하지만 추가 투약, 건너뛰기 또는 변경을 결정하지 않습니다.' }], howTo: [{ name: '처방을 입력하기', text: '수의사의 지시대로 약 이름, 첫 시간과 기간을 입력합니다.' }, { name: '시간 방식을 선택하기', text: '시간 간격 또는 처방에 적힌 매일의 시간을 선택합니다.' }, { name: '첫날 확인하기', text: '사용하기 전에 첫날과 다음 미확인 투약을 살펴봅니다.' }, { name: '투약을 표시하기', text: '투약한 뒤 행을 누르고 원래 안내문을 곁에 둡니다.' }] };
|
|
5
|
+
|
|
6
|
+
export const content = createContent(copy);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContent, type MedicationCopy } from './content';
|
|
2
|
+
import { copy as en } from './en';
|
|
3
|
+
|
|
4
|
+
const copy: MedicationCopy = { ...en, slug: 'medicatieschema-huisdier', title: 'Medicatieschema voor huisdieren', description: 'Maak van een voorgeschreven dierenartsbehandeling een duidelijk lokaal schema met volgende giften en afvinklijst.', dateLocale: 'nl', heroEyebrow: 'Volg het voorschrift. Maak de routine rustig.', heroHint: 'Vul de instructies in die je al hebt gekregen. Deze planner berekent of adviseert geen dosis.', medicationNameLabel: 'Naam van het medicijn', medicationNamePlaceholder: 'Bijvoorbeeld: voorgeschreven antibioticum', startDateLabel: 'Datum eerste gift', startTimeLabel: 'Tijd eerste gift', scheduleModeLabel: 'Hoe is het ritme voorgeschreven?', intervalMode: 'Elke paar uur', timesMode: 'Vaste tijden per dag', intervalHoursLabel: 'Uren tussen giften', timesLabel: 'Dagelijkse tijden', timesHint: 'Gebruik 24-uurs tijden met komma s ertussen, zoals 08:00, 16:00, 00:00.', durationLabel: 'Duur van de behandeling', durationUnit: 'dagen', instructionsLabel: 'Notities van het voorschrift', instructionsPlaceholder: 'Voer, toediening of andere aanwijzingen van de dierenarts', reset: 'Schema wissen', scheduleTitle: 'Jouw doseerbord', scheduleSummary: 'Een lokale checklist voor één voorgeschreven plan', nextDoseLabel: 'Volgende niet-afgevinkte gift', noNextDose: 'Alle getoonde giften zijn afgevinkt', completedCount: '{done} van {total} afgevinkt', markDone: 'Gift als gegeven markeren', markUndone: 'Gift weer open markeren', completed: 'Gedaan', upcoming: 'Binnenkort', due: 'Controleren', emptySchedule: 'Vul de velden in om het doseerbord te zien.', invalidInput: 'Controleer naam, datum, tijd, duur en ritme.', localOnlyLabel: 'Draait in je browser', safetyTitle: 'Volg het voorschrift precies', safetyText: 'Dit is een hulpmiddel voor herinneren en registreren. Het kiest geen medicijn, verandert geen dosis en vervangt geen dierenarts. Bel de voorschrijvende praktijk bij een late, vergeten of uitgebraakte gift.', methodTitle: 'Zo werkt het', methodText: 'Bij intervallen begint het schema op de eerste datum en tijd en worden de gekozen uren toegevoegd. Dagelijkse tijden worden op elke behandeldag herhaald. Alles blijft op dit apparaat tot je het wist.', scheduleIllustration: 'Met de hand getekend dagschema naast een rustige kat en hond', summary: ['Maak een bestaand voorschrift overzichtelijk.', 'Kies intervallen of vaste tijden per dag.', 'Vink giften af zonder gegevens te versturen.', 'Houd naam en opmerkingen naast de routine.'], seoTitle1: 'Een praktisch overzicht voor een bestaand dierenartsvoorschrift', seoIntro: 'Na een bezoek aan de praktijk moeten verzorgers vaak een etiket, ontslaginformatie en meerdere giften thuis combineren. De dierenarts bepaalt de behandeling; deze planner zet alleen bestaande instructies in een leesbare volgorde. Met naam, eerste lokaal moment, ritme, duur en optionele notities ontstaat een chronologische lijst voor drukke ochtenden, nachten en overdrachten tussen verzorgers.', seoTitle2: 'Twee manieren om tijden te beschrijven', seoMethod: 'Gebruik een interval als het etiket bijvoorbeeld iedere acht uur zegt. De lijst loopt dan over middernacht door. Gebruik dagelijkse tijden als ochtend en avond of concrete kloktijden zijn voorgeschreven. Op de eerste dag wordt geen eerdere tijd stil toegevoegd. De duur is een planningsvenster en geen interpretatie van het voorschrift. Het resultaat bevat tijden, geen hoeveelheden.', seoTitle3: 'Een checklist is geen medisch advies', seoSafety: 'Bewaar verpakking, praktijkinformatie en telefoonnummer in de buurt. Deze pagina kent het dier, de concentratie, toedieningsweg, interacties en reactie niet. Vraag de praktijk om advies bij een gemiste of twijfelachtige gift. Afvinken kan wel dubbele giften voorkomen en maakt de volgende taak zichtbaar. Lokale opslag is alleen gemak en kan altijd worden gewist.', tipTitle: 'Voor je begint', tipText: 'Neem frequentie en duur letterlijk over van het voorschrift. Controleer de eerste gegenereerde dag naast het etiket.', faq: [{ question: 'Berekent deze planner de hoeveelheid?', answer: 'Nee. Hij ordent alleen tijden uit een bestaand voorschrift en berekent, verandert of adviseert geen hoeveelheden.' }, { question: 'Werkt het schema over middernacht?', answer: 'Ja. Intervallen gaan door op de volgende lokale dag en dagelijkse tijden mogen 00:00 bevatten.' }, { question: 'Wat doe ik bij een gemiste gift?', answer: 'Neem contact op met de dierenarts of praktijk die het voorschrift gaf. De checklist noteert de status, maar beslist niet over inhalen, overslaan of verschuiven.' }], howTo: [{ name: 'Neem het voorschrift over', text: 'Vul naam, eerste moment en duur precies in zoals de praktijk ze gaf.' }, { name: 'Kies het ritme', text: 'Kies een interval in uren of de voorgeschreven dagelijkse tijden.' }, { name: 'Controleer het bord', text: 'Bekijk de eerste dag en de volgende open gift voordat je het als herinnering gebruikt.' }, { name: 'Vink giften af', text: 'Tik de rij aan nadat je de gift hebt gegeven en houd de originele instructies erbij.' }] };
|
|
5
|
+
|
|
6
|
+
export const content = createContent(copy);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContent, type MedicationCopy } from './content';
|
|
2
|
+
import { copy as en } from './en';
|
|
3
|
+
|
|
4
|
+
const copy: MedicationCopy = { ...en, slug: 'harmonogram-lekow-dla-zwierzat', title: 'Harmonogram leków dla zwierząt', description: 'Zamień zalecenia weterynarza w czytelny lokalny harmonogram z kolejnymi dawkami do odhaczenia.', dateLocale: 'pl', heroEyebrow: 'Trzymaj się zaleceń. Uporządkuj opiekę.', heroHint: 'Wpisz otrzymane instrukcje, aby utworzyć prostą listę. Narzędzie nie oblicza ani nie zaleca dawki.', medicationNameLabel: 'Nazwa leku', medicationNamePlaceholder: 'Na przykład: przepisany antybiotyk', startDateLabel: 'Data pierwszego podania', startTimeLabel: 'Godzina pierwszego podania', scheduleModeLabel: 'Jak określono częstotliwość?', intervalMode: 'Co kilka godzin', timesMode: 'Stałe godziny każdego dnia', intervalHoursLabel: 'Godziny między podaniami', timesLabel: 'Godziny dzienne', timesHint: 'Wpisz godziny w formacie 24-godzinnym, oddzielając je przecinkami: 08:00, 16:00, 00:00.', durationLabel: 'Czas leczenia', durationUnit: 'dni', instructionsLabel: 'Uwagi z zaleceń', instructionsPlaceholder: 'Karma, sposób podania lub inne wskazówki weterynarza', reset: 'Wyczyść harmonogram', scheduleTitle: 'Tablica podań', scheduleSummary: 'Lokalna lista kontrolna dla jednego przepisanego planu', nextDoseLabel: 'Następne nieoznaczone podanie', noNextDose: 'Wszystkie podania są oznaczone', completedCount: '{done} z {total} oznaczonych', markDone: 'Oznacz jako podane', markUndone: 'Oznacz jako niepodane', completed: 'Podano', upcoming: 'Nadchodzące', due: 'Sprawdź', emptySchedule: 'Uzupełnij pola, aby zobaczyć tablicę podań.', invalidInput: 'Sprawdź nazwę, datę, godzinę, czas trwania i częstotliwość.', localOnlyLabel: 'Działa w przeglądarce', safetyTitle: 'Stosuj dokładnie przepisane zalecenia', safetyText: 'To pomoc do przypominania i zapisu. Nie wybiera leku, nie zmienia dawki i nie zastępuje weterynarza. Przy spóźnionym, pominiętym lub zwymiotowanym podaniu skontaktuj się z lecznicą.', methodTitle: 'Jak to działa', methodText: 'Plan interwałowy zaczyna się od pierwszej daty i godziny, a następnie dodaje wybraną liczbę godzin. Godziny dzienne powtarzają się w każdym dniu leczenia. Dane pozostają na tym urządzeniu, dopóki ich nie usuniesz.', scheduleIllustration: 'Ręcznie rysowany plan dnia obok spokojnego kota i psa', summary: ['Uporządkuj istniejące zalecenia w czytelny plan.', 'Wybierz odstępy albo stałe godziny.', 'Odhacz podania bez wysyłania danych zwierzęcia.', 'Miej nazwę leku i uwagi przy codziennej rutynie.'], seoTitle1: 'Praktyczne przypomnienie dla istniejących zaleceń weterynaryjnych', seoIntro: 'Po wizycie opiekun często musi połączyć etykietę, kartę wypisu i kilka podań w domu. Decyzję o leczeniu podejmuje weterynarz; ten planer tylko porządkuje otrzymane instrukcje. Nazwa, pierwszy lokalny termin, rytm, liczba dni i własne notatki tworzą chronologiczną listę przydatną rano, w nocy i podczas przekazywania opieki.', seoTitle2: 'Dwa sposoby wpisania godzin', seoMethod: 'Wybierz interwał, gdy etykieta mówi na przykład co osiem godzin. Lista przejdzie wtedy przez północ. Wybierz godziny dzienne, gdy zalecono rano i wieczorem albo konkretne pory. Pierwszy dzień respektuje pierwsze podanie i nie dodaje wcześniejszej godziny. Czas trwania wyznacza tylko okno planowania, a wynik pokazuje pory, nie ilości leku.', seoTitle3: 'Lista kontrolna nie jest poradą medyczną', seoSafety: 'Zachowaj opakowanie, kartę z lecznicy i jej numer telefonu. Strona nie zna zwierzęcia, stężenia, drogi podania, interakcji ani reakcji organizmu. Przy pominiętym podaniu zapytaj lecznicę przed decyzją. Odznaczanie może jednak zapobiec podwójnemu podaniu i ułatwić przekazanie kolejnego zadania. Pamięć lokalną można w każdej chwili wyczyścić.', tipTitle: 'Przed rozpoczęciem', tipText: 'Przepisz częstotliwość i czas trwania z zaleceń, bez zgadywania. Porównaj pierwszy wygenerowany dzień z etykietą.', faq: [{ question: 'Czy planer oblicza ilość leku?', answer: 'Nie. Układa tylko godziny wpisane z istniejących zaleceń i nie oblicza, nie zmienia ani nie zaleca ilości.' }, { question: 'Czy obsługuje podania po północy?', answer: 'Tak. Interwał przechodzi na kolejny lokalny dzień, a godziny dzienne mogą zawierać 00:00.' }, { question: 'Co zrobić przy spóźnionym lub pominiętym podaniu?', answer: 'Skontaktuj się z weterynarzem lub lecznicą. Lista zapisuje stan, ale nie decyduje o nadrobieniu, pominięciu ani przesunięciu podania.' }], howTo: [{ name: 'Przepisz zalecenia', text: 'Wpisz nazwę, pierwszy termin i czas trwania dokładnie według zaleceń weterynarza.' }, { name: 'Wybierz rytm', text: 'Ustaw interwał godzinowy albo wpisz przepisane godziny dzienne.' }, { name: 'Sprawdź tablicę', text: 'Zweryfikuj pierwszy dzień i następne nieoznaczone podanie.' }, { name: 'Odhacz podania', text: 'Dotknij wiersza po podaniu i miej przy sobie oryginalne zalecenia.' }] };
|
|
5
|
+
|
|
6
|
+
export const content = createContent(copy);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContent, type MedicationCopy } from './content';
|
|
2
|
+
import { copy as en } from './en';
|
|
3
|
+
|
|
4
|
+
const copy: MedicationCopy = { ...en, slug: 'agenda-medicacao-animais', title: 'Agenda de medicação para animais', description: 'Transforme uma receita veterinária numa agenda local clara, com próximas doses e marcação de acompanhamento.', dateLocale: 'pt', heroEyebrow: 'Siga a receita. Simplifique a rotina.', heroHint: 'Introduza as instruções que já recebeu para criar um lembrete curto. Esta ferramenta não calcula nem recomenda doses.', medicationNameLabel: 'Nome do medicamento', medicationNamePlaceholder: 'Por exemplo: antibiótico prescrito', startDateLabel: 'Data da primeira toma', startTimeLabel: 'Hora da primeira toma', scheduleModeLabel: 'Como está indicada a frequência?', intervalMode: 'A cada algumas horas', timesMode: 'Horas fixas todos os dias', intervalHoursLabel: 'Horas entre tomas', timesLabel: 'Horas diárias', timesHint: 'Use horas de 24 horas separadas por vírgulas: 08:00, 16:00, 00:00.', durationLabel: 'Duração do tratamento', durationUnit: 'dias', instructionsLabel: 'Notas da receita', instructionsPlaceholder: 'Comida, administração ou outras instruções do veterinário', reset: 'Apagar esta agenda', scheduleTitle: 'Quadro de tomas', scheduleSummary: 'Uma lista local para uma receita prescrita', nextDoseLabel: 'Próxima toma por marcar', noNextDose: 'Todas as tomas estão marcadas', completedCount: '{done} de {total} marcadas', markDone: 'Marcar como dada', markUndone: 'Marcar como pendente', completed: 'Feita', upcoming: 'Próxima', due: 'Verificar', emptySchedule: 'Preencha os campos para ver o quadro.', invalidInput: 'Verifique nome, data, hora, duração e frequência.', localOnlyLabel: 'Funciona no navegador', safetyTitle: 'Use a receita exatamente como foi indicada', safetyText: 'É uma ajuda de memória e registo. Não escolhe medicamentos, altera doses nem substitui o veterinário. Se uma toma atrasar, for esquecida ou for vomitada, contacte a clínica.', methodTitle: 'Como funciona', methodText: 'Um intervalo começa na primeira data e hora e soma as horas escolhidas. As horas diárias repetem-se em cada dia do tratamento. Os dados ficam neste dispositivo até serem apagados.', scheduleIllustration: 'Calendário desenhado à mão junto de um gato e um cão tranquilos', summary: ['Passe uma receita existente para um horário legível.', 'Escolha intervalos ou horas fixas do dia.', 'Marque cada toma sem enviar dados do animal.', 'Mantenha o nome e as notas visíveis.'], seoTitle1: 'Um lembrete prático para uma receita veterinária existente', seoIntro: 'Depois da consulta, é comum ter de coordenar a etiqueta, a folha da clínica e várias tomas em casa. A decisão sobre o tratamento pertence ao veterinário; esta agenda apenas organiza instruções que já recebeu. O nome, o primeiro momento, o ritmo, a duração e notas opcionais tornam-se numa lista cronológica útil de manhã, durante a noite ou na troca entre cuidadores.', seoTitle2: 'Duas formas de indicar os horários', seoMethod: 'Escolha um intervalo quando a receita disser, por exemplo, a cada oito horas. A sequência atravessa a meia-noite sem perder a data local. Escolha horas diárias quando a indicação for manhã e noite ou horários concretos. No primeiro dia, uma hora anterior à primeira toma não é acrescentada. A duração é apenas uma janela de planeamento; o resultado mostra horários, não quantidades.', seoTitle3: 'Uma lista de controlo não é aconselhamento médico', seoSafety: 'Guarde a embalagem, a folha da clínica e os contactos. A página não conhece o animal, a concentração, a via, as interações ou a resposta. Em caso de toma esquecida, fale com a clínica antes de decidir. Marcar as linhas pode evitar duplicações e esclarecer a próxima ação entre cuidadores. O armazenamento local é opcional e pode ser limpo.', tipTitle: 'Antes de começar', tipText: 'Copie a frequência e a duração da receita, sem as calcular. Confirme o primeiro dia criado comparando-o com a etiqueta.', faq: [{ question: 'A agenda calcula a quantidade do medicamento?', answer: 'Não. Apenas organiza horários introduzidos a partir de uma receita existente. Não calcula, altera nem recomenda quantidades.' }, { question: 'Pode mostrar tomas depois da meia-noite?', answer: 'Sim. Os intervalos continuam no dia local seguinte e as horas diárias podem incluir 00:00.' }, { question: 'O que faço se uma toma atrasar ou for esquecida?', answer: 'Contacte o veterinário ou a clínica que indicou o tratamento. A lista regista o estado, mas não decide se deve compensar, saltar ou mudar a toma.' }], howTo: [{ name: 'Transcreva a receita', text: 'Introduza o nome, o primeiro momento e a duração como foram indicados pela equipa veterinária.' }, { name: 'Escolha o ritmo', text: 'Selecione um intervalo em horas ou escreva as horas diárias prescritas.' }, { name: 'Reveja o quadro', text: 'Confira o primeiro dia e a próxima toma por marcar antes de o usar como lembrete.' }, { name: 'Marque cada toma', text: 'Toque numa linha depois de administrar e mantenha as instruções originais por perto.' }] };
|
|
5
|
+
|
|
6
|
+
export const content = createContent(copy);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContent, type MedicationCopy } from './content';
|
|
2
|
+
import { copy as en } from './en';
|
|
3
|
+
|
|
4
|
+
const copy: MedicationCopy = { ...en, slug: 'raspisanie-lekarstv-dlya-pitomca', title: 'Расписание лекарств для питомца', description: 'Преобразуйте назначение ветеринара в понятное локальное расписание с ближайшими приёмами и отметками.', dateLocale: 'ru', heroEyebrow: 'Следуйте назначению. Упростите уход.', heroHint: 'Введите уже полученные инструкции. Планировщик не рассчитывает и не рекомендует дозу.', medicationNameLabel: 'Название лекарства', medicationNamePlaceholder: 'Например: назначенный антибиотик', startDateLabel: 'Дата первого приёма', startTimeLabel: 'Время первого приёма', scheduleModeLabel: 'Как задан режим?', intervalMode: 'Через несколько часов', timesMode: 'Фиксированное время каждый день', intervalHoursLabel: 'Часов между приёмами', timesLabel: 'Ежедневное время', timesHint: 'Введите время в 24-часовом формате через запятую: 08:00, 16:00, 00:00.', durationLabel: 'Длительность лечения', durationUnit: 'дней', instructionsLabel: 'Примечания к назначению', instructionsPlaceholder: 'Кормление, способ приёма или другие указания врача', reset: 'Очистить расписание', scheduleTitle: 'Таблица приёмов', scheduleSummary: 'Локальный список для одного назначения', nextDoseLabel: 'Следующий непомеченный приём', noNextDose: 'Все показанные приёмы отмечены', completedCount: '{done} из {total} отмечено', markDone: 'Отметить как данное', markUndone: 'Вернуть в список', completed: 'Дано', upcoming: 'Предстоит', due: 'Проверить', emptySchedule: 'Заполните поля, чтобы увидеть таблицу.', invalidInput: 'Проверьте название, дату, время, длительность и режим.', localOnlyLabel: 'Работает в браузере', safetyTitle: 'Точно следуйте назначению', safetyText: 'Это только напоминание и журнал. Инструмент не выбирает лекарство, не меняет дозу и не заменяет ветеринара. При задержке, пропуске или рвоте после приёма свяжитесь с клиникой.', methodTitle: 'Как это работает', methodText: 'Интервальный план начинается с первой даты и времени и добавляет выбранное число часов. Ежедневные часы повторяются в каждый день лечения. Данные остаются на устройстве, пока вы их не удалите.', scheduleIllustration: 'Нарисованный от руки распорядок рядом со спокойными кошкой и собакой', summary: ['Сделайте понятное расписание из готового назначения.', 'Выберите интервалы или фиксированные часы.', 'Отмечайте приёмы без отправки данных о питомце.', 'Держите название и примечания рядом с распорядком.'], seoTitle1: 'Практичное напоминание для уже назначенного лечения', seoIntro: 'После визита в клинику владельцу часто нужно совместить этикетку, выписку и несколько приёмов дома. Решение о лечении принимает ветеринар; этот планировщик лишь превращает полученные указания в удобный список. Название, первый местный момент, режим, длительность и заметки образуют хронологическую памятку для утра, ночи и передачи ухода другому человеку.', seoTitle2: 'Два способа задать время', seoMethod: 'Выберите интервал, если на этикетке написано, например, каждые восемь часов. Последовательность продолжится после полуночи. Выберите ежедневные часы, если указаны утро и вечер или точные часы. В первый день более раннее время не добавляется автоматически. Длительность ограничивает окно планирования, а результат показывает время, а не количество лекарства.', seoTitle3: 'Список не заменяет медицинский совет', seoSafety: 'Храните упаковку, выписку и номер клиники рядом. Страница не знает животное, концентрацию, путь введения, взаимодействия и реакцию. При пропущенном приёме спросите врача до принятия решения. Отметки всё же помогают избежать повторной дачи и показывают следующую задачу. Локальные данные можно удалить в любой момент.', tipTitle: 'Перед началом', tipText: 'Перепишите частоту и длительность из назначения, не угадывая. Сверьте первый день с этикеткой.', faq: [{ question: 'Рассчитывает ли планировщик дозу?', answer: 'Нет. Он только раскладывает время из существующего назначения и не рассчитывает, не меняет и не рекомендует количество.' }, { question: 'Учитывает ли он приёмы после полуночи?', answer: 'Да. Интервал переходит на следующий местный день, а среди ежедневных часов можно указать 00:00.' }, { question: 'Что делать при пропуске или задержке?', answer: 'Свяжитесь с назначившим лечение ветеринаром или клиникой. Список показывает состояние, но не решает, нужно ли наверстать, пропустить или перенести приём.' }], howTo: [{ name: 'Перенесите назначение', text: 'Укажите название, первый момент и длительность точно по инструкции врача.' }, { name: 'Выберите режим', text: 'Задайте интервал в часах или ежедневные часы из назначения.' }, { name: 'Проверьте таблицу', text: 'Сверьте первый день и следующий непомеченный приём.' }, { name: 'Отмечайте приёмы', text: 'Нажимайте строку после дачи и храните рядом исходную инструкцию.' }] };
|
|
5
|
+
|
|
6
|
+
export const content = createContent(copy);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContent, type MedicationCopy } from './content';
|
|
2
|
+
import { copy as en } from './en';
|
|
3
|
+
|
|
4
|
+
const copy: MedicationCopy = { ...en, slug: 'lakemedelsschema-for-husdjur', title: 'Läkemedelsschema för husdjur', description: 'Gör ett tydligt lokalt schema av en veterinärförskriven behandling, med kommande doser och avprickning.', dateLocale: 'sv', heroEyebrow: 'Följ ordinationen. Förenkla vardagen.', heroHint: 'Skriv in instruktionerna du redan fått. Planeraren räknar inte ut och rekommenderar ingen dos.', medicationNameLabel: 'Läkemedlets namn', medicationNamePlaceholder: 'Till exempel: förskrivet antibiotikum', startDateLabel: 'Datum för första dosen', startTimeLabel: 'Tid för första dosen', scheduleModeLabel: 'Hur anges tidsintervallet?', intervalMode: 'Med några timmars mellanrum', timesMode: 'Fasta tider varje dag', intervalHoursLabel: 'Timmar mellan doserna', timesLabel: 'Dagliga tider', timesHint: 'Använd 24-timmarsformat och separera tider med kommatecken: 08:00, 16:00, 00:00.', durationLabel: 'Behandlingens längd', durationUnit: 'dagar', instructionsLabel: 'Anteckningar från ordinationen', instructionsPlaceholder: 'Mat, hantering eller andra instruktioner från veterinären', reset: 'Rensa schemat', scheduleTitle: 'Dostavla', scheduleSummary: 'En lokal checklista för en ordinerad behandling', nextDoseLabel: 'Nästa ej avprickade dos', noNextDose: 'Alla visade doser är avprickade', completedCount: '{done} av {total} avprickade', markDone: 'Markera som given', markUndone: 'Markera som ogiven', completed: 'Klar', upcoming: 'Kommande', due: 'Kontrollera', emptySchedule: 'Fyll i fälten för att se dostavlan.', invalidInput: 'Kontrollera namn, datum, tid, längd och tidsregel.', localOnlyLabel: 'Körs i webbläsaren', safetyTitle: 'Följ ordinationen exakt', safetyText: 'Detta är ett hjälpmedel för påminnelse och dokumentation. Det väljer inget läkemedel, ändrar ingen dos och ersätter inte veterinären. Kontakta kliniken vid en försenad, glömd eller uppkräkt dos.', methodTitle: 'Så fungerar det', methodText: 'Ett intervallschema börjar vid första datum och tid och lägger till valda timmar. Dagliga tider upprepas under varje behandlingsdag. Allt sparas på enheten tills du raderar det.', scheduleIllustration: 'Handritat dagschema bredvid en lugn katt och hund', summary: ['Gör ett befintligt recept till ett lättläst schema.', 'Välj intervall eller fasta tider för dagen.', 'Pricka av doser utan att skicka djuruppgifter.', 'Ha namn och anteckningar nära rutinen.'], seoTitle1: 'En praktisk påminnelse för en befintlig veterinärordination', seoIntro: 'Efter ett klinikbesök behöver djurägaren ofta samordna etikett, utskrivningsblad och flera doser hemma. Veterinären avgör behandlingen; planeraren ordnar bara instruktioner som redan finns. Namn, första lokala tid, rytm, behandlingslängd och valfria anteckningar blir en kronologisk lista för morgnar, nätter och överlämning mellan personer.', seoTitle2: 'Två sätt att beskriva tiderna', seoMethod: 'Välj intervall när etiketten säger exempelvis var åttonde timme. Schemat fortsätter då över midnatt. Välj dagliga tider när ordinationen anger morgon och kväll eller bestämda klockslag. Den första dagen respekterar första dosen och lägger inte till en tidigare tid. Längden är ett planeringsfönster, inte en tolkning av receptet. Resultatet visar tider, inte mängder.', seoTitle3: 'En checklista är inte medicinsk rådgivning', seoSafety: 'Ha förpackning, klinikens papper och telefonnummer till hands. Sidan känner inte till djuret, koncentrationen, administreringsvägen, interaktioner eller reaktioner. Fråga kliniken om en dos missats. Avprickning kan ändå minska risken för dubbla doser och visa nästa uppgift tydligt. Lokal lagring är valfri och kan raderas när som helst.', tipTitle: 'Innan du börjar', tipText: 'Kopiera frekvens och längd från ordinationen utan att uppskatta. Kontrollera den första dagen mot etiketten.', faq: [{ question: 'Räknar planeraren ut dosen?', answer: 'Nej. Den ordnar endast tider som du skriver in från en befintlig ordination och räknar inte ut eller ändrar mängder.' }, { question: 'Fungerar tider efter midnatt?', answer: 'Ja. Intervall fortsätter på nästa lokala dag och dagliga tider kan innehålla 00:00.' }, { question: 'Vad gör jag om en dos missas?', answer: 'Kontakta veterinären eller kliniken som skrev ordinationen. Listan visar status men avgör inte om dosen ska tas igen, hoppas över eller flyttas.' }], howTo: [{ name: 'Skriv av ordinationen', text: 'Ange namn, första tid och längd precis som veterinärteamet angav.' }, { name: 'Välj rytm', text: 'Välj ett timintervall eller skriv de ordinerade dagliga tiderna.' }, { name: 'Granska tavlan', text: 'Kontrollera första dagen och nästa ej avprickade dos.' }, { name: 'Pricka av', text: 'Tryck på raden efter dosen och ha originalinstruktionen nära.' }] };
|
|
5
|
+
|
|
6
|
+
export const content = createContent(copy);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContent, type MedicationCopy } from './content';
|
|
2
|
+
import { copy as en } from './en';
|
|
3
|
+
|
|
4
|
+
const copy: MedicationCopy = { ...en, slug: 'evcil-hayvan-ilac-programi', title: 'Evcil Hayvan İlaç Programı', description: 'Veterinerin yazdığı tedavi planını, yaklaşan dozları ve işaretleme listesini içeren anlaşılır bir yerel programa dönüştürün.', dateLocale: 'tr', heroEyebrow: 'Reçeteye uyun. Rutini kolaylaştırın.', heroHint: 'Elinizdeki talimatları girin. Bu araç doz hesaplamaz ve doz önermez.', medicationNameLabel: 'İlaç adı', medicationNamePlaceholder: 'Örneğin: reçete edilen antibiyotik', startDateLabel: 'İlk doz tarihi', startTimeLabel: 'İlk doz saati', scheduleModeLabel: 'Reçetede zamanlama nasıl belirtiliyor?', intervalMode: 'Birkaç saatte bir', timesMode: 'Her gün sabit saatler', intervalHoursLabel: 'Dozlar arasındaki saat', timesLabel: 'Günlük saatler', timesHint: '24 saat biçimindeki saatleri virgülle ayırın: 08:00, 16:00, 00:00.', durationLabel: 'Tedavi süresi', durationUnit: 'gün', instructionsLabel: 'Reçete notları', instructionsPlaceholder: 'Mama, uygulama veya veterinerin diğer talimatları', reset: 'Programı temizle', scheduleTitle: 'Doz panosu', scheduleSummary: 'Tek bir reçeteli plan için yerel kontrol listesi', nextDoseLabel: 'İşaretlenmemiş sonraki doz', noNextDose: 'Listelenen tüm dozlar işaretlendi', completedCount: '{done} / {total} işaretlendi', markDone: 'Verildi olarak işaretle', markUndone: 'Verilmedi olarak işaretle', completed: 'Tamamlandı', upcoming: 'Sıradaki', due: 'Kontrol et', emptySchedule: 'Doz panosunu görmek için alanları doldurun.', invalidInput: 'Adı, tarihi, saati, süreyi ve zamanlama kuralını kontrol edin.', localOnlyLabel: 'Tarayıcıda çalışır', safetyTitle: 'Reçeteyi aynen uygulayın', safetyText: 'Bu araç yalnızca hatırlatma ve kayıt içindir. İlaç seçmez, dozu değiştirmez ve veterinerin yerini tutmaz. Geciken, unutulan veya kusulan bir dozda kliniği arayın.', methodTitle: 'Nasıl çalışır?', methodText: 'Aralıklı plan ilk tarih ve saatten başlar, seçilen saatleri ekler. Günlük saatler tedavinin her gününde tekrarlanır. Bilgiler siz silene kadar bu cihazda kalır.', scheduleIllustration: 'Sakin bir kedi ve köpeğin yanında elle çizilmiş günlük program', summary: ['Mevcut reçeteyi okunabilir bir programa çevirin.', 'Saat aralığı veya günlük sabit saat seçin.', 'Evcil hayvan bilgisi göndermeden dozları işaretleyin.', 'İlaç adını ve notları rutinin yanında tutun.'], seoTitle1: 'Mevcut veteriner reçetesi için pratik hatırlatıcı', seoIntro: 'Klinik ziyaretinden sonra bakıcıların etiketi, taburcu kâğıdını ve evdeki birden fazla dozu bir araya getirmesi gerekir. Tedavi kararını veteriner verir; bu planlayıcı yalnızca elinizdeki talimatları sıralar. İlaç adı, ilk yerel tarih ve saat, zamanlama, gün sayısı ve notlar; sabah, gece veya bakıcı değişiminde okunabilecek kronolojik bir liste oluşturur.', seoTitle2: 'Saatleri belirtmenin iki yolu', seoMethod: 'Etiket her sekiz saatte bir diyorsa aralık seçin; liste gece yarısından sonra da devam eder. Reçete sabah-akşam ya da belirli saatler söylüyorsa günlük saatleri seçin. İlk gün ilk doza uyar ve daha erken bir saati kendiliğinden eklemez. Süre yalnızca planlama aralığıdır; sonuç ilaç miktarı değil, saatleri gösterir.', seoTitle3: 'Kontrol listesi tıbbi tavsiye değildir', seoSafety: 'Kutuyu, klinik belgesini ve telefon numarasını yanınızda tutun. Sayfa hayvanı, yoğunluğu, uygulama yolunu, etkileşimleri veya yanıtı bilmez. Kaçırılan bir dozda karar vermeden önce kliniğe danışın. İşaretlemek yine de iki kez verme riskini azaltabilir ve sıradaki görevi görünür kılabilir. Yerel kayıt her zaman silinebilir.', tipTitle: 'Başlamadan önce', tipText: 'Sıklığı ve süreyi reçeteden aynen kopyalayın, tahmin etmeyin. Oluşan ilk günü etiketle karşılaştırın.', faq: [{ question: 'Program doz miktarını hesaplar mı?', answer: 'Hayır. Yalnızca mevcut reçeteden girdiğiniz saatleri düzenler; miktar hesaplamaz, değiştirmez veya önermez.' }, { question: 'Gece yarısından sonraki dozları gösterir mi?', answer: 'Evet. Aralık bir sonraki yerel güne geçer ve günlük saatler içinde 00:00 kullanılabilir.' }, { question: 'Bir doz gecikirse veya unutulursa ne yapmalıyım?', answer: 'Veterinerle veya reçeteyi yazan klinikle iletişime geçin. Liste durumu kaydeder ama dozu telafi etme, atlama veya taşıma kararı vermez.' }], howTo: [{ name: 'Reçeteyi aktarın', text: 'Adı, ilk zamanı ve süreyi veteriner ekibinin verdiği şekilde yazın.' }, { name: 'Zamanlamayı seçin', text: 'Saat aralığını veya reçetedeki günlük saatleri kullanın.' }, { name: 'Panoyu inceleyin', text: 'İlk günü ve işaretlenmemiş sonraki dozu kontrol edin.' }, { name: 'Dozları işaretleyin', text: 'Verdikten sonra satıra dokunun ve asıl talimatları yakında tutun.' }] };
|
|
5
|
+
|
|
6
|
+
export const content = createContent(copy);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContent, type MedicationCopy } from './content';
|
|
2
|
+
import { copy as en } from './en';
|
|
3
|
+
|
|
4
|
+
const copy: MedicationCopy = { ...en, slug: 'chong-wu-yong-yao-ri-cheng', title: '宠物用药日程表', description: '把兽医已经开具的用药安排整理成清晰的本地日程,显示下一次用药并支持完成标记。', dateLocale: 'zh-CN', heroEyebrow: '照处方执行,让照护更从容。', heroHint: '输入已经收到的说明,生成几天的简单时间表。本工具不会计算或推荐剂量。', medicationNameLabel: '药物名称', medicationNamePlaceholder: '例如:处方抗生素', startDateLabel: '第一次用药日期', startTimeLabel: '第一次用药时间', scheduleModeLabel: '处方如何规定时间?', intervalMode: '每隔几小时', timesMode: '每天固定时间', intervalHoursLabel: '两次用药间隔(小时)', timesLabel: '每天的时间', timesHint: '使用24小时制,用逗号分隔,例如 08:00、16:00、00:00。', durationLabel: '治疗时长', durationUnit: '天', instructionsLabel: '处方备注', instructionsPlaceholder: '进食、喂药方式或兽医的其他说明', reset: '清除日程', scheduleTitle: '用药看板', scheduleSummary: '为一份处方建立的本地检查清单', nextDoseLabel: '下一次未确认的用药', noNextDose: '显示的用药都已确认', completedCount: '已确认 {done} / {total}', markDone: '标记为已用药', markUndone: '标记为未用药', completed: '已完成', upcoming: '即将到来', due: '查看', emptySchedule: '填写上方内容后即可查看用药看板。', invalidInput: '请检查药名、日期、时间、时长和时间规则。', localOnlyLabel: '在浏览器中运行', safetyTitle: '严格按照处方执行', safetyText: '这是用于提醒和记录的辅助工具,不会选择药物、修改剂量,也不能替代兽医建议。如果用药迟了、漏服或发生呕吐,请联系开方的诊所。', methodTitle: '工作方式', methodText: '间隔模式从第一次日期和时间开始,按指定小时数生成后续时间。每日时间模式会在治疗期间重复输入的时刻。除非主动清除,信息只保存在本设备上。', scheduleIllustration: '平静的猫和狗旁边的一张手绘每日用药表', summary: ['把现有处方整理成容易阅读的时间表。', '选择时间间隔或每天固定的时刻。', '不发送宠物信息,也能标记每次用药。', '在日程旁边保留药名和备注。'], seoTitle1: '为现有兽医处方准备的实用提醒', seoIntro: '看完兽医后,照护者通常需要在家里同时参考药品标签、出院说明和多次用药安排。治疗方案由兽医决定,本工具只整理已经收到的说明。输入药名、第一次用药时间、重复方式、天数和备注,就能得到一份按时间排列的清单,适合忙碌的早晨、夜间照护以及不同照护者之间交接。', seoTitle2: '两种填写时间的方法', seoMethod: '如果标签写着每八小时一次,请选择间隔模式,日程会自然跨过午夜。如果处方写着早晚各一次或列出具体时间,请选择每天固定时间。第一天会尊重第一次用药时间,不会悄悄加入更早的时间。治疗时长只是日程范围,并不是对处方的解释;结果显示时间,不显示药量。', seoTitle3: '检查清单不是医疗建议', seoSafety: '请把药盒、诊所说明和联系电话放在手边。本页面不知道宠物情况、浓度、给药途径、相互作用或实际反应。如果漏服或不确定,请先咨询诊所。完成标记可以减少重复用药,并让下一位照护者清楚下一步。设备内保存的数据可以随时清除。', tipTitle: '开始前', tipText: '从处方原样抄录频率和时长,不要自行估计。使用前把生成的第一天与药品标签核对。', faq: [{ question: '这个工具会计算药量吗?', answer: '不会。它只整理根据现有处方输入的时间,不会计算、修改或推荐用量。' }, { question: '可以处理跨午夜的用药吗?', answer: '可以。间隔模式会进入下一个本地日期,每日时间也可以填写00:00。' }, { question: '如果用药晚了或漏了怎么办?', answer: '请联系开具处方的兽医或诊所。清单可以记录状态,但不会决定是否补用、跳过或调整时间。' }], howTo: [{ name: '录入处方信息', text: '按照兽医说明填写药名、第一次用药时间和治疗时长。' }, { name: '选择时间模式', text: '选择小时间隔,或填写处方中列出的每日时间。' }, { name: '检查日程', text: '开始使用前核对第一天和下一次未确认的用药。' }, { name: '标记每次用药', text: '完成用药后点击对应行,并把原始说明放在附近。' }] };
|
|
5
|
+
|
|
6
|
+
export const content = createContent(copy);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../types';
|
|
2
|
+
import { petMedicationSchedulePlanner } from './entry';
|
|
3
|
+
|
|
4
|
+
export * from './entry';
|
|
5
|
+
export * from './logic';
|
|
6
|
+
|
|
7
|
+
export const PET_MEDICATION_SCHEDULE_PLANNER_TOOL: ToolDefinition = {
|
|
8
|
+
entry: petMedicationSchedulePlanner,
|
|
9
|
+
Component: () => import('./component.astro'),
|
|
10
|
+
SEOComponent: () => import('./seo.astro'),
|
|
11
|
+
BibliographyComponent: () => import('./bibliography.astro'),
|
|
12
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { generateSchedule, getDateKey, type MedicationScheduleInput } from './logic';
|
|
3
|
+
|
|
4
|
+
const base: MedicationScheduleInput = {
|
|
5
|
+
medicationName: 'Prescribed medicine', startDate: '2026-09-05', startTime: '22:00', mode: 'interval', intervalHours: 8, dailyTimes: '', durationDays: 2, instructions: '',
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
describe('pet medication schedule logic', () => {
|
|
9
|
+
it('creates interval doses across midnight without changing the local date incorrectly', () => {
|
|
10
|
+
const doses = generateSchedule(base);
|
|
11
|
+
expect(doses.map((dose) => `${getDateKey(dose.date)} ${dose.time}`)).toEqual(['2026-09-05 22:00', '2026-09-06 06:00', '2026-09-06 14:00', '2026-09-06 22:00', '2026-09-07 06:00', '2026-09-07 14:00']);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('creates sorted daily-time doses and removes duplicates', () => {
|
|
15
|
+
const doses = generateSchedule({ ...base, startTime: '07:00', mode: 'times', dailyTimes: '20:00, 08:00, 08:00', durationDays: 2 });
|
|
16
|
+
expect(doses.map((dose) => dose.time)).toEqual(['08:00', '20:00', '08:00', '20:00']);
|
|
17
|
+
expect(doses[2]?.dateKey).toBe('2026-09-06');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('rejects a schedule without a medication or usable timing', () => {
|
|
21
|
+
expect(() => generateSchedule({ ...base, medicationName: '' })).toThrow('Missing medication name');
|
|
22
|
+
expect(() => generateSchedule({ ...base, mode: 'times', dailyTimes: 'later' })).toThrow('Missing times');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export type ScheduleMode = 'interval' | 'times';
|
|
2
|
+
|
|
3
|
+
export interface MedicationScheduleInput {
|
|
4
|
+
medicationName: string;
|
|
5
|
+
startDate: string;
|
|
6
|
+
startTime: string;
|
|
7
|
+
mode: ScheduleMode;
|
|
8
|
+
intervalHours: number;
|
|
9
|
+
dailyTimes: string;
|
|
10
|
+
durationDays: number;
|
|
11
|
+
instructions: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ScheduledDose {
|
|
15
|
+
id: string;
|
|
16
|
+
date: Date;
|
|
17
|
+
dateKey: string;
|
|
18
|
+
time: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getDateKey(date: Date): string {
|
|
22
|
+
return [date.getFullYear(), String(date.getMonth() + 1).padStart(2, '0'), String(date.getDate()).padStart(2, '0')].join('-');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseClock(value: string): number | null {
|
|
26
|
+
const match = /^(\d{1,2}):(\d{2})$/.exec(value.trim());
|
|
27
|
+
if (!match) return null;
|
|
28
|
+
const hours = Number(match[1]);
|
|
29
|
+
const minutes = Number(match[2]);
|
|
30
|
+
return hours <= 23 && minutes <= 59 ? hours * 60 + minutes : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseStart(input: MedicationScheduleInput): Date {
|
|
34
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(input.startDate)) throw new Error('Invalid date');
|
|
35
|
+
const minutes = parseClock(input.startTime);
|
|
36
|
+
if (minutes === null) throw new Error('Invalid time');
|
|
37
|
+
const parts = input.startDate.split('-').map(Number);
|
|
38
|
+
const year = parts[0] ?? 0;
|
|
39
|
+
const month = parts[1] ?? 0;
|
|
40
|
+
const day = parts[2] ?? 0;
|
|
41
|
+
const date = new Date(year, month - 1, day, Math.floor(minutes / 60), minutes % 60, 0, 0);
|
|
42
|
+
if (Number.isNaN(date.getTime()) || getDateKey(date) !== input.startDate) throw new Error('Invalid date');
|
|
43
|
+
return date;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function formatClock(minutes: number): string {
|
|
47
|
+
return `${String(Math.floor(minutes / 60)).padStart(2, '0')}:${String(minutes % 60).padStart(2, '0')}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createDose(date: Date): ScheduledDose {
|
|
51
|
+
const copy = new Date(date);
|
|
52
|
+
const time = formatClock(copy.getHours() * 60 + copy.getMinutes());
|
|
53
|
+
return { id: `${getDateKey(copy)}T${time}`, date: copy, dateKey: getDateKey(copy), time };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readDailyTimes(value: string): number[] {
|
|
57
|
+
const values = value.split(/[,\s]+/).map((part) => parseClock(part)).filter((part): part is number => part !== null);
|
|
58
|
+
return [...new Set(values)].sort((left, right) => left - right);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function validateScheduleInput(input: MedicationScheduleInput): void {
|
|
62
|
+
if (!input.medicationName.trim()) throw new Error('Missing medication name');
|
|
63
|
+
parseStart(input);
|
|
64
|
+
validateDuration(input.durationDays);
|
|
65
|
+
validateTiming(input);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function validateDuration(durationDays: number): void {
|
|
69
|
+
if (!Number.isInteger(durationDays) || durationDays < 1 || durationDays > 30) throw new Error('Invalid duration');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function validateTiming(input: MedicationScheduleInput): void {
|
|
73
|
+
if (input.mode === 'interval') {
|
|
74
|
+
if (!Number.isFinite(input.intervalHours) || input.intervalHours < 1 || input.intervalHours > 24) throw new Error('Invalid interval');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (readDailyTimes(input.dailyTimes).length === 0) throw new Error('Missing times');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function generateDailySchedule(input: MedicationScheduleInput, start: Date, end: Date): ScheduledDose[] {
|
|
81
|
+
const doses: ScheduledDose[] = [];
|
|
82
|
+
const times = readDailyTimes(input.dailyTimes);
|
|
83
|
+
for (let day = 0; day < input.durationDays; day += 1) {
|
|
84
|
+
for (const minutes of times) {
|
|
85
|
+
const dose = new Date(start);
|
|
86
|
+
dose.setDate(start.getDate() + day);
|
|
87
|
+
dose.setHours(Math.floor(minutes / 60), minutes % 60, 0, 0);
|
|
88
|
+
if (dose >= start && dose < end) doses.push(createDose(dose));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return doses;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function generateSchedule(input: MedicationScheduleInput): ScheduledDose[] {
|
|
95
|
+
validateScheduleInput(input);
|
|
96
|
+
const start = parseStart(input);
|
|
97
|
+
const end = new Date(start);
|
|
98
|
+
end.setDate(end.getDate() + input.durationDays);
|
|
99
|
+
const doses: ScheduledDose[] = [];
|
|
100
|
+
|
|
101
|
+
if (input.mode === 'interval') {
|
|
102
|
+
const cursor = new Date(start);
|
|
103
|
+
while (cursor < end && doses.length < 200) {
|
|
104
|
+
doses.push(createDose(cursor));
|
|
105
|
+
cursor.setTime(cursor.getTime() + input.intervalHours * 60 * 60 * 1000);
|
|
106
|
+
}
|
|
107
|
+
} else doses.push(...generateDailySchedule(input, start, end));
|
|
108
|
+
|
|
109
|
+
return doses.sort((left, right) => left.date.getTime() - right.date.getTime());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function formatTime(date: Date, locale: string): string {
|
|
113
|
+
return new Intl.DateTimeFormat(locale, { hour: 'numeric', minute: '2-digit' }).format(date);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function formatDay(date: Date, locale: string): string {
|
|
117
|
+
return new Intl.DateTimeFormat(locale, { weekday: 'long', month: 'short', day: 'numeric' }).format(date);
|
|
118
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { petMedicationSchedulePlanner } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props { locale?: KnownLocale; }
|
|
7
|
+
const { locale = 'en' } = Astro.props as Props;
|
|
8
|
+
const content = await petMedicationSchedulePlanner.i18n[locale]?.();
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
{content && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { MedicationScheduleInput } from './logic';
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEY = 'jjlmoya-pet-medication-schedule';
|
|
4
|
+
|
|
5
|
+
export interface MedicationStorageState extends MedicationScheduleInput {
|
|
6
|
+
completedIds: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function loadMedicationState(): Partial<MedicationStorageState> {
|
|
10
|
+
try {
|
|
11
|
+
const raw = window.localStorage.getItem(STORAGE_KEY);
|
|
12
|
+
return raw ? JSON.parse(raw) as Partial<MedicationStorageState> : {};
|
|
13
|
+
} catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function saveMedicationState(state: MedicationStorageState): void {
|
|
19
|
+
try {
|
|
20
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
|
21
|
+
} catch {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function clearMedicationState(): void {
|
|
27
|
+
try {
|
|
28
|
+
window.localStorage.removeItem(STORAGE_KEY);
|
|
29
|
+
} catch {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from '../../types';
|
|
2
|
+
|
|
3
|
+
export interface PetMedicationSchedulePlannerUI {
|
|
4
|
+
[key: string]: string;
|
|
5
|
+
dateLocale: string;
|
|
6
|
+
heroEyebrow: string;
|
|
7
|
+
heroHint: string;
|
|
8
|
+
medicationNameLabel: string;
|
|
9
|
+
medicationNamePlaceholder: string;
|
|
10
|
+
startDateLabel: string;
|
|
11
|
+
startTimeLabel: string;
|
|
12
|
+
scheduleModeLabel: string;
|
|
13
|
+
intervalMode: string;
|
|
14
|
+
timesMode: string;
|
|
15
|
+
intervalHoursLabel: string;
|
|
16
|
+
timesLabel: string;
|
|
17
|
+
timesHint: string;
|
|
18
|
+
durationLabel: string;
|
|
19
|
+
durationUnit: string;
|
|
20
|
+
instructionsLabel: string;
|
|
21
|
+
instructionsPlaceholder: string;
|
|
22
|
+
reset: string;
|
|
23
|
+
scheduleTitle: string;
|
|
24
|
+
scheduleSummary: string;
|
|
25
|
+
nextDoseLabel: string;
|
|
26
|
+
noNextDose: string;
|
|
27
|
+
completedCount: string;
|
|
28
|
+
markDone: string;
|
|
29
|
+
markUndone: string;
|
|
30
|
+
completed: string;
|
|
31
|
+
upcoming: string;
|
|
32
|
+
due: string;
|
|
33
|
+
emptySchedule: string;
|
|
34
|
+
invalidInput: string;
|
|
35
|
+
localOnlyLabel: string;
|
|
36
|
+
safetyTitle: string;
|
|
37
|
+
safetyText: string;
|
|
38
|
+
methodTitle: string;
|
|
39
|
+
methodText: string;
|
|
40
|
+
scheduleIllustration: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type PetMedicationSchedulePlannerLocaleContent = ToolLocaleContent<PetMedicationSchedulePlannerUI>;
|
package/src/tools.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { PET_GESTATION_TOOL } from './tool/petGestation';
|
|
|
5
5
|
import { PET_TOXICITY_TOOL } from './tool/petToxicity';
|
|
6
6
|
import { PET_WATER_INTAKE_TOOL } from './tool/petWaterIntake';
|
|
7
7
|
import { PET_CARRIER_CRATE_SIZE_PLANNER_TOOL } from './tool/petCarrierCrateSizePlanner';
|
|
8
|
+
import { PET_MEDICATION_SCHEDULE_PLANNER_TOOL } from './tool/petMedicationSchedulePlanner';
|
|
8
9
|
import type { ToolDefinition } from './types';
|
|
9
10
|
|
|
10
11
|
export const ALL_TOOLS: ToolDefinition[] = [
|
|
@@ -14,6 +15,7 @@ export const ALL_TOOLS: ToolDefinition[] = [
|
|
|
14
15
|
PET_TOXICITY_TOOL,
|
|
15
16
|
PET_WATER_INTAKE_TOOL,
|
|
16
17
|
PET_CARRIER_CRATE_SIZE_PLANNER_TOOL,
|
|
18
|
+
PET_MEDICATION_SCHEDULE_PLANNER_TOOL,
|
|
17
19
|
];
|
|
18
20
|
|
|
19
21
|
export {
|
|
@@ -23,4 +25,5 @@ export {
|
|
|
23
25
|
PET_TOXICITY_TOOL,
|
|
24
26
|
PET_WATER_INTAKE_TOOL,
|
|
25
27
|
PET_CARRIER_CRATE_SIZE_PLANNER_TOOL,
|
|
28
|
+
PET_MEDICATION_SCHEDULE_PLANNER_TOOL,
|
|
26
29
|
};
|