feat: add checkbox to include consumption from today until planner start date (#98)
- Add 'Include consumption from today until start date' checkbox to planner - When checked, usage calculation starts from today instead of max(today, startDate) - Persist checkbox state in localStorage per user - Add i18n translations (EN + DE) - Update planner tests to use dynamic future dates
This commit is contained in:
@@ -495,10 +495,14 @@ export async function medicationRoutes(app: FastifyInstance) {
|
||||
});
|
||||
|
||||
app.post("/medications/usage", async (req, reply) => {
|
||||
const schema = z.object({ startDate: z.string().datetime(), endDate: z.string().datetime() });
|
||||
const schema = z.object({
|
||||
startDate: z.string().datetime(),
|
||||
endDate: z.string().datetime(),
|
||||
includeUntilStart: z.boolean().optional().default(false),
|
||||
});
|
||||
const parsed = schema.safeParse(req.body);
|
||||
if (!parsed.success) return reply.status(400).send(parsed.error.format());
|
||||
const { startDate, endDate } = parsed.data;
|
||||
const { startDate, endDate, includeUntilStart } = parsed.data;
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime()) || end <= start) {
|
||||
@@ -507,6 +511,30 @@ export async function medicationRoutes(app: FastifyInstance) {
|
||||
|
||||
const userId = await getUserId(req, reply);
|
||||
const rows = await db.select().from(medications).where(eq(medications.userId, userId)).orderBy(medications.id);
|
||||
|
||||
// Get all taken doses for this user to calculate actual consumption
|
||||
const takenDoses = await db
|
||||
.select()
|
||||
.from(doseTracking)
|
||||
.where(and(eq(doseTracking.userId, userId), eq(doseTracking.dismissed, false)));
|
||||
|
||||
// Create a map of medication ID to taken dose count
|
||||
const takenDosesMap = new Map<number, { blisterIdx: number; usage: number }[]>();
|
||||
takenDoses.forEach((dose) => {
|
||||
const parts = dose.doseId.split("-");
|
||||
if (parts.length >= 3) {
|
||||
const medId = parseInt(parts[0], 10);
|
||||
const blisterIdx = parseInt(parts[1], 10);
|
||||
if (!Number.isNaN(medId) && !Number.isNaN(blisterIdx)) {
|
||||
if (!takenDosesMap.has(medId)) {
|
||||
takenDosesMap.set(medId, []);
|
||||
}
|
||||
takenDosesMap.get(medId)!.push({ blisterIdx, usage: 0 }); // usage filled later
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Use current time as the reference point for "available" stock
|
||||
const now = new Date();
|
||||
|
||||
const payload = rows.map((row) => {
|
||||
@@ -517,7 +545,6 @@ export async function medicationRoutes(app: FastifyInstance) {
|
||||
row.intakeRemindersEnabled ?? false
|
||||
);
|
||||
const blisters = intakes.map((i) => ({ usage: i.usage, every: i.every, start: i.start }));
|
||||
const usageTotal = calculateUsageInRange(blisters, start, end);
|
||||
const pillsPerBlister = row.pillsPerBlister ?? 1;
|
||||
const packCount = row.packCount ?? 1;
|
||||
const blistersPerPack = row.blistersPerPack ?? 1;
|
||||
@@ -525,25 +552,80 @@ export async function medicationRoutes(app: FastifyInstance) {
|
||||
const stockAdjustment = row.stockAdjustment ?? 0;
|
||||
const originalTotalPills = packCount * blistersPerPack * pillsPerBlister + looseTablets + stockAdjustment;
|
||||
|
||||
// Calculate consumption up to now (same logic as frontend)
|
||||
// Calculate consumption based on ACTUAL taken doses from dose_tracking
|
||||
// This ensures Planner shows the same "current stock" as the Dashboard/Modal
|
||||
// Use the same logic as frontend: generate expected doses and check which are marked
|
||||
const stockCorrectionCutoff = row.lastStockCorrectionAt ? new Date(row.lastStockCorrectionAt).getTime() : 0;
|
||||
|
||||
// Build a Set of taken dose IDs for quick lookup
|
||||
const takenDoseIds = new Set(
|
||||
takenDoses
|
||||
.filter((dose) => {
|
||||
const parts = dose.doseId.split("-");
|
||||
return parts.length >= 3 && parseInt(parts[0], 10) === row.id;
|
||||
})
|
||||
.map((dose) => dose.doseId)
|
||||
);
|
||||
|
||||
// Count consumed pills by generating expected doses and checking if they're taken
|
||||
let consumedUntilNow = 0;
|
||||
blisters.forEach((blister) => {
|
||||
const msPerDay = 86400000;
|
||||
|
||||
blisters.forEach((blister, blisterIdx) => {
|
||||
const blisterStart = parseLocalDateTime(blister.start);
|
||||
if (Number.isNaN(blisterStart.getTime()) || blisterStart > now) return;
|
||||
const msPerDay = 86400000;
|
||||
if (Number.isNaN(blisterStart.getTime())) return;
|
||||
|
||||
const effectiveStart = Math.max(blisterStart.getTime(), stockCorrectionCutoff);
|
||||
if (effectiveStart > now.getTime()) return;
|
||||
|
||||
const period = Math.max(1, blister.every) * msPerDay;
|
||||
const occurrences = Math.floor((now.getTime() - blisterStart.getTime()) / period) + 1;
|
||||
consumedUntilNow += occurrences * blister.usage;
|
||||
const occurrences = Math.floor((now.getTime() - effectiveStart) / period) + 1;
|
||||
|
||||
// Get the people for this intake (from intakes array or medication takenBy)
|
||||
const takenByJson = row.takenByJson ? JSON.parse(row.takenByJson) : [];
|
||||
const intake = intakes[blisterIdx];
|
||||
const intakePerson = intake?.takenBy;
|
||||
const peopleForThisIntake: (string | null)[] = intakePerson
|
||||
? [intakePerson]
|
||||
: takenByJson.length > 0
|
||||
? takenByJson
|
||||
: [null];
|
||||
|
||||
// Generate expected dose IDs and check if they're taken
|
||||
for (let i = 0; i < occurrences; i++) {
|
||||
const doseDate = new Date(effectiveStart + i * period);
|
||||
const dateOnlyMs = new Date(doseDate.getFullYear(), doseDate.getMonth(), doseDate.getDate()).getTime();
|
||||
const baseDoseId = `${row.id}-${blisterIdx}-${dateOnlyMs}`;
|
||||
|
||||
// Check if each person has taken this dose
|
||||
for (const person of peopleForThisIntake) {
|
||||
const doseId = person ? `${baseDoseId}-${person}` : baseDoseId;
|
||||
if (takenDoseIds.has(doseId)) {
|
||||
consumedUntilNow += blister.usage;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const currentPills = Math.max(0, originalTotalPills - consumedUntilNow);
|
||||
const currentStock = Math.max(0, originalTotalPills - consumedUntilNow);
|
||||
|
||||
// Calculate usage for the planning period
|
||||
// When includeUntilStart is true, calculate from now to end (useful for trip planning)
|
||||
// When false, calculate from max(now, start) to end (default behavior)
|
||||
const effectivePlannerStart = includeUntilStart ? now : new Date(Math.max(now.getTime(), start.getTime()));
|
||||
const usageTotal = calculateUsageInRange(blisters, effectivePlannerStart, end);
|
||||
|
||||
const blistersNeeded = pillsPerBlister > 0 ? Math.ceil(usageTotal / pillsPerBlister) : 0;
|
||||
|
||||
// Calculate current stock using realistic consumption order (loose first, then blisters)
|
||||
const consumed = originalTotalPills - currentPills;
|
||||
const looseConsumed = Math.min(consumed, looseTablets);
|
||||
const loosePillsRemaining = looseTablets - looseConsumed;
|
||||
const blisterPillsConsumed = consumed - looseConsumed;
|
||||
// Calculate AVAILABLE = stock AFTER the planned period (currentStock - usageTotal)
|
||||
const availableAfterPeriod = Math.max(0, currentStock - usageTotal);
|
||||
|
||||
// Calculate stock breakdown for availableAfterPeriod
|
||||
// Consumption order: loose pills first, then from blisters
|
||||
const totalConsumedByEnd = originalTotalPills - availableAfterPeriod;
|
||||
const looseConsumedByEnd = Math.min(totalConsumedByEnd, looseTablets);
|
||||
const loosePillsRemaining = Math.max(0, looseTablets - looseConsumedByEnd);
|
||||
const blisterPillsConsumed = totalConsumedByEnd - looseConsumedByEnd;
|
||||
const originalBlisterPills = originalTotalPills - looseTablets;
|
||||
const blisterPillsRemaining = Math.max(0, originalBlisterPills - blisterPillsConsumed);
|
||||
|
||||
@@ -551,11 +633,11 @@ export async function medicationRoutes(app: FastifyInstance) {
|
||||
const openBlisterPills = pillsPerBlister > 0 ? blisterPillsRemaining % pillsPerBlister : 0;
|
||||
const loosePills = loosePillsRemaining + openBlisterPills; // Combine open blister + remaining loose
|
||||
|
||||
const enough = currentPills >= usageTotal;
|
||||
const enough = currentStock >= usageTotal;
|
||||
return {
|
||||
medicationId: row.id,
|
||||
medicationName: row.name,
|
||||
totalPills: currentPills,
|
||||
totalPills: currentStock,
|
||||
plannerUsage: usageTotal,
|
||||
blisterSize: pillsPerBlister,
|
||||
blistersNeeded,
|
||||
|
||||
@@ -706,7 +706,16 @@ describe("Integration Tests", () => {
|
||||
describe("Planner usage calculation", () => {
|
||||
it("should calculate correct usage for daily medication", async () => {
|
||||
// Create medication: 2 packs × 3 blisters × 10 pills = 60 pills total
|
||||
// Schedule: 1 pill daily starting Jan 1
|
||||
// Schedule: 1 pill daily starting tomorrow (future date)
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setHours(8, 0, 0, 0);
|
||||
const intakeStart = tomorrow.toISOString();
|
||||
|
||||
const planEnd = new Date(tomorrow);
|
||||
planEnd.setDate(planEnd.getDate() + 10);
|
||||
const planEndStr = planEnd.toISOString();
|
||||
|
||||
await app.inject({
|
||||
method: "POST",
|
||||
url: "/medications",
|
||||
@@ -716,17 +725,17 @@ describe("Integration Tests", () => {
|
||||
blistersPerPack: 3,
|
||||
pillsPerBlister: 10,
|
||||
looseTablets: 0,
|
||||
blisters: [{ usage: 1, every: 1, start: "2025-01-01T08:00:00.000Z" }],
|
||||
blisters: [{ usage: 1, every: 1, start: intakeStart }],
|
||||
},
|
||||
});
|
||||
|
||||
// Calculate usage for Jan 1-10 (10 days = 10 pills needed)
|
||||
// Calculate usage for 10 days starting tomorrow
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
url: "/medications/usage",
|
||||
payload: {
|
||||
startDate: "2025-01-01T00:00:00.000Z",
|
||||
endDate: "2025-01-11T00:00:00.000Z", // 10 days
|
||||
startDate: intakeStart,
|
||||
endDate: planEndStr, // 10 days
|
||||
},
|
||||
});
|
||||
|
||||
@@ -735,13 +744,22 @@ describe("Integration Tests", () => {
|
||||
expect(data).toHaveLength(1);
|
||||
expect(data[0].medicationName).toBe("Daily Med");
|
||||
expect(data[0].plannerUsage).toBe(10); // 10 days × 1 pill
|
||||
// Note: 'enough' depends on current stock after consumption since start date
|
||||
// Since test runs ~364 days after Jan 1, most pills are consumed
|
||||
expect(data[0].totalPills).toBe(60); // Current stock is full (no consumption yet)
|
||||
expect(data[0].enough).toBe(true);
|
||||
});
|
||||
|
||||
it("should detect insufficient stock", async () => {
|
||||
// Create medication: 1 pack × 1 blister × 5 pills = 5 pills total
|
||||
// Schedule: 1 pill daily
|
||||
// Schedule: 1 pill daily starting tomorrow
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setHours(8, 0, 0, 0);
|
||||
const intakeStart = tomorrow.toISOString();
|
||||
|
||||
const planEnd = new Date(tomorrow);
|
||||
planEnd.setDate(planEnd.getDate() + 10);
|
||||
const planEndStr = planEnd.toISOString();
|
||||
|
||||
await app.inject({
|
||||
method: "POST",
|
||||
url: "/medications",
|
||||
@@ -751,17 +769,17 @@ describe("Integration Tests", () => {
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 5,
|
||||
looseTablets: 0,
|
||||
blisters: [{ usage: 1, every: 1, start: "2025-01-01T08:00:00.000Z" }],
|
||||
blisters: [{ usage: 1, every: 1, start: intakeStart }],
|
||||
},
|
||||
});
|
||||
|
||||
// Calculate usage for 10 days (needs 10 pills, only have 5 originally)
|
||||
// Calculate usage for 10 days (needs 10 pills, only have 5)
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
url: "/medications/usage",
|
||||
payload: {
|
||||
startDate: "2025-01-01T00:00:00.000Z",
|
||||
endDate: "2025-01-11T00:00:00.000Z",
|
||||
startDate: intakeStart,
|
||||
endDate: planEndStr,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -773,7 +791,16 @@ describe("Integration Tests", () => {
|
||||
|
||||
it("should calculate weekly medication usage correctly", async () => {
|
||||
// Create medication: 10 pills total
|
||||
// Schedule: 1 pill every 7 days starting Jan 1
|
||||
// Schedule: 1 pill every 7 days starting tomorrow
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setHours(8, 0, 0, 0);
|
||||
const intakeStart = tomorrow.toISOString();
|
||||
|
||||
const planEnd = new Date(tomorrow);
|
||||
planEnd.setDate(planEnd.getDate() + 35); // 35 days to get 5 weekly doses
|
||||
const planEndStr = planEnd.toISOString();
|
||||
|
||||
await app.inject({
|
||||
method: "POST",
|
||||
url: "/medications",
|
||||
@@ -782,29 +809,42 @@ describe("Integration Tests", () => {
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 10,
|
||||
blisters: [{ usage: 1, every: 7, start: "2025-01-01T08:00:00.000Z" }],
|
||||
blisters: [{ usage: 1, every: 7, start: intakeStart }],
|
||||
},
|
||||
});
|
||||
|
||||
// Calculate usage for 30 days (should need ~4-5 pills)
|
||||
// Calculate usage for 35 days (should need 5 pills)
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
url: "/medications/usage",
|
||||
payload: {
|
||||
startDate: "2025-01-01T00:00:00.000Z",
|
||||
endDate: "2025-01-31T00:00:00.000Z", // 30 days
|
||||
startDate: intakeStart,
|
||||
endDate: planEndStr,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
const data = response.json();
|
||||
// Jan 1, 8, 15, 22, 29 = 5 doses
|
||||
// Day 0, 7, 14, 21, 28 = 5 doses
|
||||
expect(data[0].plannerUsage).toBe(5);
|
||||
});
|
||||
|
||||
it("should handle multiple intake schedules per medication", async () => {
|
||||
// Create medication with morning and evening doses
|
||||
// 30 pills total, 1.5 pills per day (1 morning + 0.5 evening)
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setHours(8, 0, 0, 0);
|
||||
const morningStart = tomorrow.toISOString();
|
||||
|
||||
const eveningStart = new Date(tomorrow);
|
||||
eveningStart.setHours(20, 0, 0, 0);
|
||||
const eveningStartStr = eveningStart.toISOString();
|
||||
|
||||
const planEnd = new Date(tomorrow);
|
||||
planEnd.setDate(planEnd.getDate() + 10);
|
||||
const planEndStr = planEnd.toISOString();
|
||||
|
||||
await app.inject({
|
||||
method: "POST",
|
||||
url: "/medications",
|
||||
@@ -814,8 +854,8 @@ describe("Integration Tests", () => {
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
blisters: [
|
||||
{ usage: 1, every: 1, start: "2025-01-01T08:00:00.000Z" }, // Morning: 1 pill
|
||||
{ usage: 0.5, every: 1, start: "2025-01-01T20:00:00.000Z" }, // Evening: 0.5 pill
|
||||
{ usage: 1, every: 1, start: morningStart }, // Morning: 1 pill
|
||||
{ usage: 0.5, every: 1, start: eveningStartStr }, // Evening: 0.5 pill
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -825,8 +865,8 @@ describe("Integration Tests", () => {
|
||||
method: "POST",
|
||||
url: "/medications/usage",
|
||||
payload: {
|
||||
startDate: "2025-01-01T00:00:00.000Z",
|
||||
endDate: "2025-01-11T00:00:00.000Z",
|
||||
startDate: morningStart,
|
||||
endDate: planEndStr,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -838,6 +878,15 @@ describe("Integration Tests", () => {
|
||||
|
||||
it("should calculate correct blisters needed", async () => {
|
||||
// 10 pills per blister, need 25 pills → need 3 blisters
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setHours(8, 0, 0, 0);
|
||||
const intakeStart = tomorrow.toISOString();
|
||||
|
||||
const planEnd = new Date(tomorrow);
|
||||
planEnd.setDate(planEnd.getDate() + 10);
|
||||
const planEndStr = planEnd.toISOString();
|
||||
|
||||
await app.inject({
|
||||
method: "POST",
|
||||
url: "/medications",
|
||||
@@ -846,7 +895,7 @@ describe("Integration Tests", () => {
|
||||
packCount: 5,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 10,
|
||||
blisters: [{ usage: 2.5, every: 1, start: "2025-01-01T08:00:00.000Z" }],
|
||||
blisters: [{ usage: 2.5, every: 1, start: intakeStart }],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -855,8 +904,8 @@ describe("Integration Tests", () => {
|
||||
method: "POST",
|
||||
url: "/medications/usage",
|
||||
payload: {
|
||||
startDate: "2025-01-01T00:00:00.000Z",
|
||||
endDate: "2025-01-11T00:00:00.000Z",
|
||||
startDate: intakeStart,
|
||||
endDate: planEndStr,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -176,6 +176,7 @@
|
||||
"badge": "Vorrat planen",
|
||||
"from": "Von",
|
||||
"until": "Bis",
|
||||
"includeUntilStart": "Verbrauch von heute bis Startdatum einrechnen",
|
||||
"calculate": "Berechnen",
|
||||
"calculating": "Wird berechnet...",
|
||||
"sendEmail": "📧 Per E-Mail senden",
|
||||
|
||||
@@ -176,6 +176,7 @@
|
||||
"badge": "Plan your supply",
|
||||
"from": "From",
|
||||
"until": "Until",
|
||||
"includeUntilStart": "Include consumption from today until start date",
|
||||
"calculate": "Calculate",
|
||||
"calculating": "Calculating...",
|
||||
"sendEmail": "📧 Send via Email",
|
||||
|
||||
@@ -41,6 +41,7 @@ export function PlannerPage() {
|
||||
start: toInputValue(todayIso()),
|
||||
end: toInputValue(plusDaysIso(3)),
|
||||
});
|
||||
const [includeUntilStart, setIncludeUntilStart] = useState(false);
|
||||
const [sendingPlannerEmail, setSendingPlannerEmail] = useState(false);
|
||||
const [plannerEmailResult, setPlannerEmailResult] = useState<{ success: boolean; message: string } | null>(null);
|
||||
|
||||
@@ -49,6 +50,7 @@ export function PlannerPage() {
|
||||
if (typeof window !== "undefined" && user?.id) {
|
||||
const savedRows = localStorage.getItem(userStorageKey(user.id, "plannerRows"));
|
||||
const savedRange = localStorage.getItem(userStorageKey(user.id, "plannerRange"));
|
||||
const savedIncludeUntilStart = localStorage.getItem(userStorageKey(user.id, "plannerIncludeUntilStart"));
|
||||
|
||||
if (savedRows) {
|
||||
try {
|
||||
@@ -69,16 +71,23 @@ export function PlannerPage() {
|
||||
} else {
|
||||
setRange({ start: toInputValue(todayIso()), end: toInputValue(plusDaysIso(3)) });
|
||||
}
|
||||
|
||||
if (savedIncludeUntilStart) {
|
||||
setIncludeUntilStart(savedIncludeUntilStart === "true");
|
||||
} else {
|
||||
setIncludeUntilStart(false);
|
||||
}
|
||||
} else {
|
||||
setPlannerRows([]);
|
||||
setRange({ start: toInputValue(todayIso()), end: toInputValue(plusDaysIso(3)) });
|
||||
setIncludeUntilStart(false);
|
||||
}
|
||||
}, [user?.id]);
|
||||
|
||||
async function runPlanner(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setPlannerLoading(true);
|
||||
const body = { startDate: toIsoString(range.start), endDate: toIsoString(range.end) };
|
||||
const body = { startDate: toIsoString(range.start), endDate: toIsoString(range.end), includeUntilStart };
|
||||
const rows = (await fetch("/api/medications/usage", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -93,15 +102,18 @@ export function PlannerPage() {
|
||||
if (user?.id) {
|
||||
localStorage.setItem(userStorageKey(user.id, "plannerRange"), JSON.stringify(range));
|
||||
localStorage.setItem(userStorageKey(user.id, "plannerRows"), JSON.stringify(rows));
|
||||
localStorage.setItem(userStorageKey(user.id, "plannerIncludeUntilStart"), String(includeUntilStart));
|
||||
}
|
||||
}
|
||||
|
||||
function resetRange() {
|
||||
setRange({ start: toInputValue(todayIso()), end: toInputValue(plusDaysIso(3)) });
|
||||
setIncludeUntilStart(false);
|
||||
setPlannerRows([]);
|
||||
if (user?.id) {
|
||||
localStorage.removeItem(userStorageKey(user.id, "plannerRange"));
|
||||
localStorage.removeItem(userStorageKey(user.id, "plannerRows"));
|
||||
localStorage.removeItem(userStorageKey(user.id, "plannerIncludeUntilStart"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,6 +171,14 @@ export function PlannerPage() {
|
||||
onChange={(e) => setRange({ ...range, end: e.target.value })}
|
||||
/>
|
||||
</label>
|
||||
<label className="planner-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includeUntilStart}
|
||||
onChange={(e) => setIncludeUntilStart(e.target.checked)}
|
||||
/>
|
||||
{t("planner.includeUntilStart")}
|
||||
</label>
|
||||
<div className="planner-actions">
|
||||
<button type="button" className="ghost" onClick={resetRange}>
|
||||
{t("common.reset")}
|
||||
|
||||
@@ -2019,6 +2019,25 @@ textarea.auto-resize {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.planner-checkbox {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
.planner-checkbox input[type="checkbox"] {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
cursor: pointer;
|
||||
accent-color: var(--accent-primary);
|
||||
}
|
||||
.planner-actions {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user