cd78f72f33
Complete rename of all technical identifiers across the codebase: Package names (11 packages): - @planarchy/* → @capakraken/* in all package.json, tsconfig, imports Import statements: 277 files, 548 occurrences replaced Database & Docker: - PostgreSQL user/db: planarchy → capakraken - Docker volumes: planarchy_pgdata → capakraken_pgdata - Connection strings updated in docker-compose, .env, CI CI/CD: - GitHub Actions workflow: all filter commands updated - Test database credentials updated Infrastructure: - Redis channel: planarchy:sse → capakraken:sse - Logger service name: planarchy-api → capakraken-api - Anonymization seed updated - Start/stop/restart scripts updated Test data: - Seed emails: @planarchy.dev → @capakraken.dev - E2E test credentials: all 11 spec files updated - Email defaults: @planarchy.app → @capakraken.app - localStorage keys: planarchy_* → capakraken_* Documentation: 30+ .md files updated Verification: - pnpm install: workspace resolution works - TypeScript: only pre-existing TS2589 (no new errors) - Engine: 310/310 tests pass - Staffing: 37/37 tests pass Co-Authored-By: claude-flow <ruv@ruv.net>
204 lines
7.1 KiB
TypeScript
204 lines
7.1 KiB
TypeScript
"use client";
|
||
|
||
import { RecurrenceFrequency } from "@capakraken/shared";
|
||
import type { RecurrencePattern } from "@capakraken/shared";
|
||
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
|
||
|
||
const WEEKDAY_LABELS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||
|
||
interface RecurrenceEditorProps {
|
||
value: RecurrencePattern | undefined;
|
||
onChange: (pattern: RecurrencePattern | undefined) => void;
|
||
}
|
||
|
||
export function RecurrenceEditor({ value, onChange }: RecurrenceEditorProps) {
|
||
const freq = value?.frequency ?? RecurrenceFrequency.WEEKLY;
|
||
|
||
function update(patch: Partial<RecurrencePattern>) {
|
||
onChange({ ...value, frequency: freq, ...patch });
|
||
}
|
||
|
||
function setFrequency(f: RecurrenceFrequency) {
|
||
// Reset pattern-specific fields when switching frequency
|
||
onChange({ frequency: f });
|
||
}
|
||
|
||
function toggleWeekday(dow: number) {
|
||
const current = value?.weekdays ?? [];
|
||
const next = current.includes(dow)
|
||
? current.filter((d) => d !== dow)
|
||
: [...current, dow].sort((a, b) => a - b);
|
||
update({ weekdays: next });
|
||
}
|
||
|
||
const inputClass =
|
||
"px-2 py-1 border border-gray-300 dark:border-gray-600 rounded text-sm focus:outline-none focus:ring-2 focus:ring-brand-500 dark:bg-gray-900 dark:text-gray-100";
|
||
const labelClass = "text-xs font-medium text-gray-600 dark:text-gray-400 block mb-1";
|
||
|
||
return (
|
||
<div className="space-y-3 p-3 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700">
|
||
{/* Frequency selector */}
|
||
<div>
|
||
<span className={labelClass}>Frequency<InfoTooltip content="How often the allocation repeats: weekly, biweekly, monthly, or a custom pattern." /></span>
|
||
<div className="flex gap-2 flex-wrap">
|
||
{Object.values(RecurrenceFrequency).map((f) => (
|
||
<button
|
||
key={f}
|
||
type="button"
|
||
onClick={() => setFrequency(f)}
|
||
className={`px-3 py-1 text-xs rounded-full border transition-colors ${
|
||
freq === f
|
||
? "bg-brand-600 text-white border-brand-600"
|
||
: "border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:border-brand-400"
|
||
}`}
|
||
>
|
||
{f === RecurrenceFrequency.WEEKLY
|
||
? "Weekly"
|
||
: f === RecurrenceFrequency.BIWEEKLY
|
||
? "Biweekly"
|
||
: f === RecurrenceFrequency.MONTHLY
|
||
? "Monthly"
|
||
: "Custom"}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Weekday picker — WEEKLY and BIWEEKLY */}
|
||
{(freq === RecurrenceFrequency.WEEKLY || freq === RecurrenceFrequency.BIWEEKLY) && (
|
||
<div>
|
||
<span className={labelClass}>Days of week<InfoTooltip content="Select which days of the week this allocation is active. Hours per day applies only on selected days." /></span>
|
||
<div className="flex gap-1">
|
||
{WEEKDAY_LABELS.map((label, dow) => {
|
||
const selected = (value?.weekdays ?? []).includes(dow);
|
||
return (
|
||
<button
|
||
key={dow}
|
||
type="button"
|
||
onClick={() => toggleWeekday(dow)}
|
||
className={`w-9 h-9 text-xs rounded-full border font-medium transition-colors ${
|
||
selected
|
||
? "bg-brand-600 text-white border-brand-600"
|
||
: "border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:border-brand-400"
|
||
}`}
|
||
>
|
||
{label}
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Biweekly interval */}
|
||
{freq === RecurrenceFrequency.BIWEEKLY && (
|
||
<div>
|
||
<label className={labelClass}>Every N weeks</label>
|
||
<input
|
||
type="number"
|
||
min={2}
|
||
max={8}
|
||
value={value?.interval ?? 2}
|
||
onChange={(e) => update({ interval: Number(e.target.value) })}
|
||
className={`${inputClass} w-24`}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* Monthly — day of month */}
|
||
{freq === RecurrenceFrequency.MONTHLY && (
|
||
<div>
|
||
<label className={labelClass}>Day of month (1–31)</label>
|
||
<input
|
||
type="number"
|
||
min={1}
|
||
max={31}
|
||
value={value?.monthDay ?? 1}
|
||
onChange={(e) => update({ monthDay: Number(e.target.value) })}
|
||
className={`${inputClass} w-24`}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* Custom — hoursPerDay override */}
|
||
{freq === RecurrenceFrequency.CUSTOM && (
|
||
<div>
|
||
<label className={labelClass}>Hours per active day</label>
|
||
<input
|
||
type="number"
|
||
min={0.5}
|
||
max={24}
|
||
step={0.5}
|
||
value={value?.hoursPerDay ?? 8}
|
||
onChange={(e) => update({ hoursPerDay: Number(e.target.value) })}
|
||
className={`${inputClass} w-24`}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* Optional hours override for WEEKLY/BIWEEKLY/MONTHLY */}
|
||
{freq !== RecurrenceFrequency.CUSTOM && (
|
||
<div>
|
||
<label className={labelClass}>Hours per recurring day (optional override)<InfoTooltip content="Override the allocation's default hours for recurring days only. Leave empty to use the allocation's hours/day." /></label>
|
||
<input
|
||
type="number"
|
||
min={0.5}
|
||
max={24}
|
||
step={0.5}
|
||
placeholder="Use allocation default"
|
||
value={value?.hoursPerDay ?? ""}
|
||
onChange={(e) => {
|
||
const next = { ...value, frequency: freq } as RecurrencePattern;
|
||
if (e.target.value === "") {
|
||
delete next.hoursPerDay;
|
||
} else {
|
||
next.hoursPerDay = Number(e.target.value);
|
||
}
|
||
onChange(next);
|
||
}}
|
||
className={`${inputClass} w-40`}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* Optional date range overrides */}
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label className={labelClass}>Recurrence start (optional)</label>
|
||
<input
|
||
type="date"
|
||
value={value?.startDate ?? ""}
|
||
onChange={(e) => {
|
||
const next = { ...value, frequency: freq } as RecurrencePattern;
|
||
if (e.target.value) {
|
||
next.startDate = e.target.value;
|
||
} else {
|
||
delete next.startDate;
|
||
}
|
||
onChange(next);
|
||
}}
|
||
className={inputClass}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className={labelClass}>Recurrence end (optional)</label>
|
||
<input
|
||
type="date"
|
||
value={value?.endDate ?? ""}
|
||
onChange={(e) => {
|
||
const next = { ...value, frequency: freq } as RecurrencePattern;
|
||
if (e.target.value) {
|
||
next.endDate = e.target.value;
|
||
} else {
|
||
delete next.endDate;
|
||
}
|
||
onChange(next);
|
||
}}
|
||
className={inputClass}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|