remove enabled state

This commit is contained in:
2026-05-24 13:18:57 +00:00
parent 193940ac17
commit 5edd75a8e9
32 changed files with 201 additions and 447 deletions
-7
View File
@@ -36,7 +36,6 @@ Reference data for where plants live.
| `id` | `int` | Yes | Primary key |
| `name` | `string` | Yes | User-facing location name |
| `notes` | `string` | No | Optional details |
| `is_enabled` | `bool` | Yes | Whether the location is available for new assignments |
## `CareAction`
@@ -47,7 +46,6 @@ A reusable care verb, such as water, prune, fertilize, inspect, or repot.
| `id` | `int` | Yes | Primary key |
| `name` | `string` | Yes | User-facing action name |
| `description` | `string` | No | Optional explanation |
| `is_enabled` | `bool` | Yes | Whether the action is available for use |
## `ActionResource`
@@ -57,9 +55,7 @@ A resource used while performing care.
| - | - | - | - |
| `id` | `int` | Yes | Primary key |
| `name` | `string` | Yes | Resource name |
| `category` | `string` | No | Optional grouping, such as `Fertilizer`, `Medium`, `Treatment`, `Equipment`, or `Container` |
| `notes` | `string` | No | Optional details |
| `is_enabled` | `bool` | Yes | Whether the resource is available for use |
## `CareActivity`
@@ -70,7 +66,6 @@ A configurable care activity made from one or more care actions.
| `id` | `int` | Yes | Primary key |
| `name` | `string` | Yes | User-facing activity name |
| `notes` | `string` | No | Optional details |
| `is_enabled` | `bool` | Yes | Whether the activity is available for schedules and logs |
## `CareActivityAction`
@@ -114,7 +109,6 @@ A per-plant recurring schedule for one care activity. Scheduler is the UI owner
| `ends_mode` | `string` | Yes | `on` or `after` |
| `ends_on` | `date` | No | End date when `ends_mode` is `on` |
| `ends_after_occurrences` | `int` | No | Occurrence limit when `ends_mode` is `after` |
| `is_enabled` | `bool` | Yes | Whether this schedule contributes to care tasks |
## `ActionLog`
@@ -150,7 +144,6 @@ A reusable plant flag.
| `id` | `int` | Yes | Primary key |
| `name` | `string` | Yes | Flag name |
| `color` | `string` | Yes | Display color |
| `is_enabled` | `bool` | Yes | Whether the flag can be assigned |
## `PlantFlag`
+114 -44
View File
@@ -167,6 +167,76 @@ export function App() {
}
}
async function loadLocations() {
const locationsResponse = await getPlantLocations();
setError(null);
setPlantLocations(locationsResponse);
}
async function loadPlants() {
const plantsResponse = await getPlants();
setError(null);
setPlants(plantsResponse);
}
async function loadPlantsAndCareTasks() {
const [plantsResponse, tasksResponse] = await Promise.all([
getPlants(),
getCareTasks(),
]);
setError(null);
setPlants(plantsResponse);
setCareTasks(tasksResponse);
}
async function loadTaxaAndPlants() {
const [plantsResponse, taxaResponse] = await Promise.all([
getPlants(),
getPlantTaxa(),
]);
setError(null);
setPlants(plantsResponse);
setPlantTaxa(taxaResponse);
}
async function loadFlagsAndPlants() {
const [plantsResponse, flagsResponse] = await Promise.all([
getPlants(),
getPlantFlags(),
]);
setError(null);
setPlants(plantsResponse);
setPlantFlagDefinitions(flagsResponse);
}
async function loadCareModel() {
const [
plantsResponse,
tasksResponse,
actionsResponse,
resourcesResponse,
activitiesResponse,
] = await Promise.all([
getPlants(),
getCareTasks(),
getCareActions(),
getActionResources(),
getCareActivities(),
]);
setError(null);
setPlants(plantsResponse);
setCareTasks(tasksResponse);
setCareActions(actionsResponse);
setActionResources(resourcesResponse);
setCareActivities(activitiesResponse);
}
useEffect(() => {
queueMicrotask(() => {
void loadDashboard();
@@ -201,15 +271,15 @@ export function App() {
setTaxonForm((current) => ({ ...current, [field]: value }));
}
function updateLocationForm(field: keyof LocationFormState, value: string | boolean) {
function updateLocationForm(field: keyof LocationFormState, value: string) {
setLocationForm((current) => ({ ...current, [field]: value }));
}
function updateActionForm(field: keyof ActionFormState, value: string | boolean) {
function updateActionForm(field: keyof ActionFormState, value: string) {
setActionForm((current) => ({ ...current, [field]: value }));
}
function updateResourceForm(field: keyof ResourceFormState, value: string | boolean) {
function updateResourceForm(field: keyof ResourceFormState, value: string) {
setResourceForm((current) => ({ ...current, [field]: value }));
}
@@ -217,7 +287,7 @@ export function App() {
setActivityForm((current) => ({ ...current, [field]: value }));
}
function updateFlagDefinitionForm(field: keyof FlagDefinitionFormState, value: string | boolean) {
function updateFlagDefinitionForm(field: keyof FlagDefinitionFormState, value: string) {
setFlagDefinitionForm((current) => ({ ...current, [field]: value }));
}
@@ -227,7 +297,7 @@ export function App() {
function updateBulkScheduleForm(
field: keyof BulkScheduleFormState,
value: string | boolean | string[],
value: string | string[],
) {
setBulkScheduleForm((current) => ({ ...current, [field]: value }));
}
@@ -414,7 +484,7 @@ export function App() {
await updatePlant(editingPlantId, payload);
}
cancelEditing();
await loadDashboard();
await loadPlantsAndCareTasks();
} catch {
setError('Could not save the plant.');
} finally {
@@ -438,7 +508,7 @@ export function App() {
locationId: nextValues.locationId === undefined ? selectedPlant.locationId : nextValues.locationId,
careSchedules: null,
});
await loadDashboard();
await loadPlants();
} catch {
setError('Could not update the plant assignment.');
} finally {
@@ -474,7 +544,7 @@ export function App() {
if (editingPlantId === plant.id) {
cancelEditing();
}
await loadDashboard();
await loadPlantsAndCareTasks();
} catch {
setError('Could not delete the plant.');
} finally {
@@ -497,7 +567,7 @@ export function App() {
await updatePlantTaxon(editingTaxonId, payload);
}
cancelEditingTaxon();
await loadDashboard();
await loadTaxaAndPlants();
} catch {
setError('Could not save the taxon.');
} finally {
@@ -517,7 +587,7 @@ export function App() {
if (editingTaxonId === taxon.id) {
cancelEditingTaxon();
}
await loadDashboard();
await loadTaxaAndPlants();
} catch {
setError('Could not delete the taxon. It may still be used by a plant.');
} finally {
@@ -540,7 +610,7 @@ export function App() {
await updatePlantLocation(editingLocationId, payload);
}
cancelEditingLocation();
await loadDashboard();
await loadLocations();
} catch {
setError('Could not save the location.');
} finally {
@@ -549,7 +619,7 @@ export function App() {
}
async function removeLocation(location: PlantLocation) {
const confirmed = window.confirm(`Delete ${location.name}? Locations in use will be disabled instead.`);
const confirmed = window.confirm(`Delete ${location.name}? Locations assigned to plants cannot be deleted.`);
if (!confirmed) {
return;
}
@@ -560,10 +630,10 @@ export function App() {
if (editingLocationId === location.id) {
cancelEditingLocation();
}
await loadDashboard();
await loadLocations();
} catch {
await loadDashboard();
setError('Could not delete the location. If it is in use, it was disabled instead.');
await loadLocations();
setError('Could not delete the location. It may still be assigned to a plant.');
} finally {
setIsSaving(false);
}
@@ -584,7 +654,7 @@ export function App() {
await updateCareAction(editingActionId, payload);
}
cancelEditingAction();
await loadDashboard();
await loadCareModel();
} catch {
setError('Could not save the action.');
} finally {
@@ -593,7 +663,7 @@ export function App() {
}
async function removeAction(action: CareAction) {
const confirmed = window.confirm(`Delete ${action.name}? Actions with care history will be disabled instead.`);
const confirmed = window.confirm(`Delete ${action.name}? Actions with care history cannot be deleted.`);
if (!confirmed) {
return;
}
@@ -604,10 +674,10 @@ export function App() {
if (editingActionId === action.id) {
cancelEditingAction();
}
await loadDashboard();
await loadCareModel();
} catch {
await loadDashboard();
setError('Could not delete the action. If it has care history, it was disabled instead.');
await loadCareModel();
setError('Could not delete the action. It may still have care history.');
} finally {
setIsSaving(false);
}
@@ -628,7 +698,7 @@ export function App() {
await updateActionResource(editingResourceId, payload);
}
cancelEditingResource();
await loadDashboard();
await loadCareModel();
} catch {
setError('Could not save the resource.');
} finally {
@@ -648,7 +718,7 @@ export function App() {
if (editingResourceId === resource.id) {
cancelEditingResource();
}
await loadDashboard();
await loadCareModel();
} catch {
setError('Could not delete the resource.');
} finally {
@@ -671,7 +741,7 @@ export function App() {
await updateCareActivity(editingActivityId, payload);
}
cancelEditingActivity();
await loadDashboard();
await loadCareModel();
} catch {
setError('Could not save the activity.');
} finally {
@@ -680,7 +750,7 @@ export function App() {
}
async function removeActivity(activity: CareActivity) {
const confirmed = window.confirm(`Delete ${activity.name}? Activities in use will be disabled instead.`);
const confirmed = window.confirm(`Delete ${activity.name}? Activities in use cannot be deleted.`);
if (!confirmed) {
return;
}
@@ -691,10 +761,10 @@ export function App() {
if (editingActivityId === activity.id) {
cancelEditingActivity();
}
await loadDashboard();
await loadCareModel();
} catch {
await loadDashboard();
setError('Could not delete the activity. If it is in use, it was disabled instead.');
await loadCareModel();
setError('Could not delete the activity. It may still be in use.');
} finally {
setIsSaving(false);
}
@@ -715,7 +785,7 @@ export function App() {
await updatePlantFlag(editingFlagDefinitionId, payload);
}
cancelEditingFlagDefinition();
await loadDashboard();
await loadFlagsAndPlants();
} catch {
setError('Could not save the plant flag.');
} finally {
@@ -733,7 +803,7 @@ export function App() {
try {
await savePlantCareSchedulesBulk(toBulkSchedulePayload(bulkScheduleForm));
setBulkScheduleForm(emptyBulkScheduleForm);
await loadDashboard();
await loadPlantsAndCareTasks();
} catch {
setError('Could not apply the care schedule.');
} finally {
@@ -751,7 +821,7 @@ export function App() {
try {
await removePlantCareSchedulesBulk(toBulkSchedulePayload(bulkScheduleForm));
setBulkScheduleForm(emptyBulkScheduleForm);
await loadDashboard();
await loadPlantsAndCareTasks();
} catch {
setError('Could not remove the care schedule.');
} finally {
@@ -760,7 +830,7 @@ export function App() {
}
async function removeFlagDefinition(flag: PlantFlagDefinition) {
const confirmed = window.confirm(`Delete ${flag.name}? Flags assigned to plants will be disabled instead.`);
const confirmed = window.confirm(`Delete ${flag.name}? Flags assigned to plants cannot be deleted.`);
if (!confirmed) {
return;
}
@@ -771,10 +841,10 @@ export function App() {
if (editingFlagDefinitionId === flag.id) {
cancelEditingFlagDefinition();
}
await loadDashboard();
await loadFlagsAndPlants();
} catch {
await loadDashboard();
setError('Could not delete the flag. If it is assigned to plants, it was disabled instead.');
await loadFlagsAndPlants();
setError('Could not delete the flag. It may still be assigned to a plant.');
} finally {
setIsSaving(false);
}
@@ -790,7 +860,7 @@ export function App() {
try {
await assignPlantFlag(selectedPlantId, toPlantFlagPayload(plantFlagForm));
setPlantFlagForm(emptyPlantFlagForm);
await loadDashboard();
await loadPlants();
} catch {
setError('Could not attach the plant flag.');
} finally {
@@ -806,7 +876,7 @@ export function App() {
setIsSaving(true);
try {
await resolvePlantFlag(selectedPlantId, flag.id);
await loadDashboard();
await loadPlants();
} catch {
setError('Could not resolve the plant flag.');
} finally {
@@ -827,7 +897,7 @@ export function App() {
setIsSaving(true);
try {
await removePlantFlagAssignment(selectedPlantId, flag.id);
await loadDashboard();
await loadPlants();
} catch {
setError('Could not remove the plant flag.');
} finally {
@@ -845,7 +915,7 @@ export function App() {
notes: '',
resources: [],
});
await loadDashboard();
await loadPlantsAndCareTasks();
} catch {
setError('Could not log the care task.');
} finally {
@@ -874,7 +944,7 @@ export function App() {
notes: '',
resources: [],
});
await loadDashboard();
await loadPlantsAndCareTasks();
} catch {
setError('Could not log the due tasks.');
} finally {
@@ -985,11 +1055,11 @@ export function App() {
<section className="catalog-help" aria-label="Catalog status">
<h3>Catalog Status</h3>
<p>{dueCount} due</p>
<p>{plantLocations.filter((location) => location.isEnabled).length} active locations</p>
<p>{careActivities.filter((activity) => activity.isEnabled).length} active activities</p>
<p>{careActions.filter((action) => action.isEnabled).length} active actions</p>
<p>{actionResources.filter((resource) => resource.isEnabled).length} active resources</p>
<p>{plantFlagDefinitions.filter((flag) => flag.isEnabled).length} active flags</p>
<p>{plantLocations.length} locations</p>
<p>{careActivities.length} activities</p>
<p>{careActions.length} actions</p>
<p>{actionResources.length} resources</p>
<p>{plantFlagDefinitions.length} flags</p>
</section>
</aside>
@@ -15,7 +15,7 @@ type ActionsViewProps = {
onCloseDetail: () => void;
onDelete: (action: CareAction) => void;
onEdit: (action: CareAction) => void;
onFieldChange: (field: keyof ActionFormState, value: string | boolean) => void;
onFieldChange: (field: keyof ActionFormState, value: string) => void;
onNew: () => void;
onOpenDetail: (action: CareAction) => void;
onSave: () => void;
@@ -39,15 +39,13 @@ export function ActionsView({
onOpenDetail,
onSave,
}: ActionsViewProps) {
const enabledCount = actions.filter((action) => action.isEnabled).length;
return (
<>
<section className="summary-panel" aria-labelledby="actions-summary-heading">
<div>
<p className="eyebrow">Care menu</p>
<h2 id="actions-summary-heading">
{isLoading ? 'Loading actions' : `${enabledCount} actions enabled`}
{isLoading ? 'Loading actions' : `${actions.length} actions`}
</h2>
<p>{error ?? 'Configure the care actions available when logging plant work.'}</p>
</div>
@@ -78,10 +76,6 @@ export function ActionsView({
<span>Description</span>
<strong>{selectedAction.description ?? 'No description'}</strong>
</div>
<div>
<span>Status</span>
<strong>{selectedAction.isEnabled ? 'Enabled' : 'Disabled'}</strong>
</div>
</div>
</section>
) : null}
@@ -113,14 +107,6 @@ export function ActionsView({
onChange={(event) => onFieldChange('description', event.target.value)}
/>
</label>
<label className="toggle-field">
<input
checked={form.isEnabled}
type="checkbox"
onChange={(event) => onFieldChange('isEnabled', event.target.checked)}
/>
Enabled
</label>
</div>
<div className="form-actions">
@@ -151,8 +137,6 @@ export function ActionsView({
<h3>{action.name}</h3>
<p>
{action.description ?? 'No description'}
{' - '}
{action.isEnabled ? 'Enabled' : 'Disabled'}
</p>
</div>
<div className="row-actions">
@@ -46,10 +46,6 @@ export function ActivitiesView({
onSave,
resources,
}: ActivitiesViewProps) {
const enabledCount = activities.filter((activity) => activity.isEnabled).length;
const enabledActions = actions.filter((action) => action.isEnabled);
const enabledResources = resources.filter((resource) => resource.isEnabled);
function updateAction(index: number, nextAction: ActivityFormState['actions'][number]) {
onFieldChange(
'actions',
@@ -77,7 +73,7 @@ export function ActivitiesView({
<div>
<p className="eyebrow">Care activities</p>
<h2 id="activities-summary-heading">
{isLoading ? 'Loading activities' : `${enabledCount} activities enabled`}
{isLoading ? 'Loading activities' : `${activities.length} activities`}
</h2>
<p>{error ?? 'Configure reusable care bundles with actions and per-action resources.'}</p>
</div>
@@ -108,10 +104,6 @@ export function ActivitiesView({
<span>Notes</span>
<strong>{selectedActivity.notes ?? 'No notes'}</strong>
</div>
<div>
<span>Status</span>
<strong>{selectedActivity.isEnabled ? 'Enabled' : 'Disabled'}</strong>
</div>
</div>
<div className="detail-list">
{selectedActivity.actions.length === 0 ? (
@@ -192,7 +184,7 @@ export function ActivitiesView({
)}
>
<option value="">Select an action</option>
{enabledActions
{actions
.filter((action) => !selectedActionIds.has(String(action.id)))
.map((action) => (
<option key={action.id} value={action.id}>
@@ -238,7 +230,7 @@ export function ActivitiesView({
)}
>
<option value="">Select a resource</option>
{enabledResources
{resources
.filter((resource) => !selectedResourceIds.has(String(resource.id)))
.map((resource) => (
<option key={resource.id} value={resource.id}>
@@ -309,7 +301,7 @@ export function ActivitiesView({
<button
className="small-action"
type="button"
disabled={enabledResources.length === 0}
disabled={resources.length === 0}
onClick={() => updateAction(
actionIndex,
{
@@ -341,7 +333,7 @@ export function ActivitiesView({
<button
className="small-action"
type="button"
disabled={enabledActions.length === 0}
disabled={actions.length === 0}
onClick={addAction}
>
<Plus size={16} />
@@ -355,14 +347,6 @@ export function ActivitiesView({
onChange={(event) => onFieldChange('notes', event.target.value)}
/>
</label>
<label className="toggle-field">
<input
checked={form.isEnabled}
type="checkbox"
onChange={(event) => onFieldChange('isEnabled', event.target.checked)}
/>
Enabled
</label>
</div>
<div className="form-actions">
@@ -391,7 +375,7 @@ export function ActivitiesView({
<article className="plant-row" key={activity.id}>
<div>
<h3>{activity.name}</h3>
<p>{formatActivitySummary(activity)} - {activity.isEnabled ? 'Enabled' : 'Disabled'}</p>
<p>{formatActivitySummary(activity)}</p>
</div>
<div className="row-actions">
<button className="icon-button compact" type="button" aria-label={`View ${activity.name}`} onClick={() => onOpenDetail(activity)}>
+2 -17
View File
@@ -15,7 +15,7 @@ type FlagsViewProps = {
onCloseDetail: () => void;
onDelete: (flag: PlantFlagDefinition) => void;
onEdit: (flag: PlantFlagDefinition) => void;
onFieldChange: (field: keyof FlagDefinitionFormState, value: string | boolean) => void;
onFieldChange: (field: keyof FlagDefinitionFormState, value: string) => void;
onNew: () => void;
onOpenDetail: (flag: PlantFlagDefinition) => void;
onSave: () => void;
@@ -39,15 +39,13 @@ export function FlagsView({
onOpenDetail,
onSave,
}: FlagsViewProps) {
const enabledCount = flags.filter((flag) => flag.isEnabled).length;
return (
<>
<section className="summary-panel" aria-labelledby="flags-summary-heading">
<div>
<p className="eyebrow">Plant flags</p>
<h2 id="flags-summary-heading">
{isLoading ? 'Loading flags' : `${enabledCount} flags enabled`}
{isLoading ? 'Loading flags' : `${flags.length} flags`}
</h2>
<p>{error ?? 'Configure reusable flags for plants.'}</p>
</div>
@@ -82,10 +80,6 @@ export function FlagsView({
</span>
</strong>
</div>
<div>
<span>Status</span>
<strong>{selectedFlag.isEnabled ? 'Enabled' : 'Disabled'}</strong>
</div>
</div>
</section>
) : null}
@@ -118,14 +112,6 @@ export function FlagsView({
onChange={(event) => onFieldChange('color', event.target.value)}
/>
</label>
<label className="toggle-field">
<input
checked={form.isEnabled}
type="checkbox"
onChange={(event) => onFieldChange('isEnabled', event.target.checked)}
/>
Enabled
</label>
</div>
<div className="form-actions">
@@ -154,7 +140,6 @@ export function FlagsView({
<article className="plant-row" key={flag.id}>
<div>
<h3>{flag.name}</h3>
<p>{flag.isEnabled ? 'Enabled' : 'Disabled'}</p>
</div>
<div className="row-actions">
<span className="flag-chip" style={{ backgroundColor: flag.color }}>
@@ -15,7 +15,7 @@ type LocationsViewProps = {
onCloseDetail: () => void;
onDelete: (location: PlantLocation) => void;
onEdit: (location: PlantLocation) => void;
onFieldChange: (field: keyof LocationFormState, value: string | boolean) => void;
onFieldChange: (field: keyof LocationFormState, value: string) => void;
onNew: () => void;
onOpenDetail: (location: PlantLocation) => void;
onSave: () => void;
@@ -39,15 +39,13 @@ export function LocationsView({
onOpenDetail,
onSave,
}: LocationsViewProps) {
const enabledCount = locations.filter((location) => location.isEnabled).length;
return (
<>
<section className="summary-panel" aria-labelledby="locations-summary-heading">
<div>
<p className="eyebrow">Location library</p>
<h2 id="locations-summary-heading">
{isLoading ? 'Loading locations' : `${enabledCount} locations enabled`}
{isLoading ? 'Loading locations' : `${locations.length} locations`}
</h2>
<p>{error ?? 'Create and maintain the places where plants live.'}</p>
</div>
@@ -78,10 +76,6 @@ export function LocationsView({
<span>Notes</span>
<strong>{selectedLocation.notes ?? 'No notes'}</strong>
</div>
<div>
<span>Status</span>
<strong>{selectedLocation.isEnabled ? 'Enabled' : 'Disabled'}</strong>
</div>
</div>
</section>
) : null}
@@ -113,14 +107,6 @@ export function LocationsView({
onChange={(event) => onFieldChange('notes', event.target.value)}
/>
</label>
<label className="toggle-field">
<input
checked={form.isEnabled}
type="checkbox"
onChange={(event) => onFieldChange('isEnabled', event.target.checked)}
/>
Enabled
</label>
</div>
<div className="form-actions">
@@ -151,8 +137,6 @@ export function LocationsView({
<h3>{location.name}</h3>
<p>
{location.notes ?? 'No notes'}
{' - '}
{location.isEnabled ? 'Enabled' : 'Disabled'}
</p>
</div>
<div className="row-actions">
@@ -48,9 +48,6 @@ export function PlantManagementView({
onSetLocation,
onSetTaxon,
}: PlantManagementViewProps) {
const enabledFlags = plantFlagDefinitions.filter((flag) => flag.isEnabled);
const enabledLocations = plantLocations.filter((location) =>
location.isEnabled || location.id === selectedPlant?.locationId);
const selectedTaxon = plantTaxa.find((taxon) => taxon.id === selectedPlant?.taxonId);
const selectedLocation = plantLocations.find((location) => location.id === selectedPlant?.locationId);
const activeFlags = selectedPlant?.flags.filter((flag) => flag.resolvedOn === null) ?? [];
@@ -148,9 +145,9 @@ export function PlantManagementView({
onChange={(event) => onSetLocation(event.target.value)}
>
<option value="">No location</option>
{enabledLocations.map((location) => (
{plantLocations.map((location) => (
<option key={location.id} value={location.id}>
{location.isEnabled ? location.name : `${location.name} (disabled)`}
{location.name}
</option>
))}
</select>
@@ -162,10 +159,6 @@ export function PlantManagementView({
<span>Location</span>
<strong>{selectedLocation.name}</strong>
</div>
<div>
<span>Status</span>
<strong>{selectedLocation.isEnabled ? 'Enabled' : 'Disabled'}</strong>
</div>
{selectedLocation.notes ? (
<div className="meta-wide">
<span>Notes</span>
@@ -202,7 +195,7 @@ export function PlantManagementView({
onChange={(event) => onFieldChange('plantFlagDefinitionId', event.target.value)}
>
<option value="">Select a flag</option>
{enabledFlags.map((flag) => (
{plantFlagDefinitions.map((flag) => (
<option key={flag.id} value={flag.id}>
{flag.name}
</option>
@@ -14,7 +14,7 @@ type ResourcesViewProps = {
onCloseDetail: () => void;
onDelete: (resource: ActionResource) => void;
onEdit: (resource: ActionResource) => void;
onFieldChange: (field: keyof ResourceFormState, value: string | boolean) => void;
onFieldChange: (field: keyof ResourceFormState, value: string) => void;
onNew: () => void;
onOpenDetail: (resource: ActionResource) => void;
onSave: () => void;
@@ -39,15 +39,13 @@ export function ResourcesView({
onSave,
resources,
}: ResourcesViewProps) {
const enabledCount = resources.filter((resource) => resource.isEnabled).length;
return (
<>
<section className="summary-panel" aria-labelledby="resources-summary-heading">
<div>
<p className="eyebrow">Resource library</p>
<h2 id="resources-summary-heading">
{isLoading ? 'Loading resources' : `${enabledCount} resources enabled`}
{isLoading ? 'Loading resources' : `${resources.length} resources`}
</h2>
<p>{error ?? 'Configure materials, products, tools, and containers used during care.'}</p>
</div>
@@ -74,18 +72,10 @@ export function ResourcesView({
</div>
</div>
<div className="plant-detail-meta">
<div>
<span>Category</span>
<strong>{selectedResource.category ?? 'Uncategorized'}</strong>
</div>
<div>
<span>Notes</span>
<strong>{selectedResource.notes ?? 'No notes'}</strong>
</div>
<div>
<span>Status</span>
<strong>{selectedResource.isEnabled ? 'Enabled' : 'Disabled'}</strong>
</div>
</div>
</section>
) : null}
@@ -110,13 +100,6 @@ export function ResourcesView({
onChange={(event) => onFieldChange('name', event.target.value)}
/>
</label>
<label>
Category
<input
value={form.category}
onChange={(event) => onFieldChange('category', event.target.value)}
/>
</label>
<label>
Notes
<input
@@ -124,14 +107,6 @@ export function ResourcesView({
onChange={(event) => onFieldChange('notes', event.target.value)}
/>
</label>
<label className="toggle-field">
<input
checked={form.isEnabled}
type="checkbox"
onChange={(event) => onFieldChange('isEnabled', event.target.checked)}
/>
Enabled
</label>
</div>
<div className="form-actions">
@@ -161,11 +136,7 @@ export function ResourcesView({
<div>
<h3>{resource.name}</h3>
<p>
{resource.category ?? 'Uncategorized'}
{' - '}
{resource.notes ?? 'No notes'}
{' - '}
{resource.isEnabled ? 'Enabled' : 'Disabled'}
</p>
</div>
<div className="row-actions">
@@ -10,7 +10,7 @@ type SchedulesViewProps = {
isLoading: boolean;
isSaving: boolean;
plants: Plant[];
onFieldChange: (field: keyof BulkScheduleFormState, value: string | boolean | string[]) => void;
onFieldChange: (field: keyof BulkScheduleFormState, value: string | string[]) => void;
onRemove: () => void;
onSave: () => void;
};
@@ -46,7 +46,6 @@ export function SchedulesView({
onSave,
}: SchedulesViewProps) {
const [plantQuery, setPlantQuery] = useState('');
const enabledActivities = activities.filter((activity) => activity.isEnabled);
const visiblePlants = useMemo(
() => filterPlants(plants, plantQuery),
[plants, plantQuery],
@@ -93,7 +92,7 @@ export function SchedulesView({
onChange={(event) => onFieldChange('careActivityId', event.target.value)}
>
<option value="">Select an activity</option>
{enabledActivities.map((activity) => (
{activities.map((activity) => (
<option key={activity.id} value={activity.id}>
{activity.name}
</option>
@@ -109,15 +108,6 @@ export function SchedulesView({
onChange={(event) => onFieldChange('scheduledFor', event.target.value)}
/>
</label>
<label className="toggle-field">
<input
checked={form.isEnabled}
disabled={isSaving}
type="checkbox"
onChange={(event) => onFieldChange('isEnabled', event.target.checked)}
/>
Enabled
</label>
</div>
<fieldset className="schedule-options">
@@ -348,7 +338,7 @@ export function SchedulesView({
<div className="schedule-chip-list">
{plant.careSchedules.map((schedule) => (
<span className={`status-pill ${schedule.status}`} key={schedule.id}>
{schedule.action} / {formatRecurrence(schedule)} / {schedule.isEnabled ? schedule.nextCare : 'Disabled'}
{schedule.action} / {formatRecurrence(schedule)} / {schedule.nextCare}
</span>
))}
</div>
@@ -372,7 +362,7 @@ function formatPlantScheduleSummary(plant: Plant, selectedActivity?: CareActivit
return `No ${selectedActivity.name} schedule`;
}
return `${selectedActivity.name}: ${formatRecurrence(schedule)} - ${schedule.isEnabled ? schedule.nextCare : 'disabled'}`;
return `${selectedActivity.name}: ${formatRecurrence(schedule)} - ${schedule.nextCare}`;
}
function filterPlants(plants: Plant[], query: string) {
-16
View File
@@ -31,7 +31,6 @@ export type PlantCareSchedule = {
lastPerformed: string;
nextCare: string;
status: CareStatus;
isEnabled: boolean;
};
export type ScheduleRecurrenceMode = 'none' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'custom';
@@ -52,22 +51,18 @@ export type PlantLocation = {
id: number;
name: string;
notes: string | null;
isEnabled: boolean;
};
export type CareAction = {
id: number;
name: string;
description: string | null;
isEnabled: boolean;
};
export type ActionResource = {
id: number;
name: string;
category: string | null;
notes: string | null;
isEnabled: boolean;
};
export type CareActivity = {
@@ -77,7 +72,6 @@ export type CareActivity = {
action: string;
actions: CareActivityAction[];
notes: string | null;
isEnabled: boolean;
};
export type CareActivityAction = {
@@ -91,7 +85,6 @@ export type CareActivityAction = {
export type CareActivityActionResource = {
actionResourceId: number;
name: string;
category: string | null;
quantity: number | null;
unit: string | null;
notes: string | null;
@@ -101,7 +94,6 @@ export type PlantFlagDefinition = {
id: number;
name: string;
color: string;
isEnabled: boolean;
};
export type PlantFlag = {
@@ -132,7 +124,6 @@ export type PlantCareSchedulePayload = {
endsMode: ScheduleEndsMode;
endsOn: string | null;
endsAfterOccurrences: number | null;
isEnabled: boolean;
};
export type BulkPlantCareSchedulePayload = {
@@ -147,7 +138,6 @@ export type BulkPlantCareSchedulePayload = {
endsMode: ScheduleEndsMode;
endsOn: string | null;
endsAfterOccurrences: number | null;
isEnabled: boolean;
};
export type PlantTaxonPayload = {
@@ -162,27 +152,22 @@ export type PlantTaxonPayload = {
export type PlantLocationPayload = {
name: string;
notes: string | null;
isEnabled: boolean;
};
export type CareActionPayload = {
name: string;
description: string | null;
isEnabled: boolean;
};
export type ActionResourcePayload = {
name: string;
category: string | null;
notes: string | null;
isEnabled: boolean;
};
export type CareActivityPayload = {
name: string;
actions: CareActivityActionPayload[];
notes: string | null;
isEnabled: boolean;
};
export type CareActivityActionPayload = {
@@ -200,7 +185,6 @@ export type CareActivityActionResourcePayload = {
export type PlantFlagDefinitionPayload = {
name: string;
color: string | null;
isEnabled: boolean;
};
export type AssignPlantFlagPayload = {
-20
View File
@@ -36,27 +36,22 @@ export const emptyTaxonForm = {
export const emptyLocationForm = {
name: '',
notes: '',
isEnabled: true,
};
export const emptyActionForm = {
name: '',
description: '',
isEnabled: true,
};
export const emptyResourceForm = {
name: '',
category: '',
notes: '',
isEnabled: true,
};
export const emptyActivityForm = {
name: '',
actions: [] as CareActivityActionFormState[],
notes: '',
isEnabled: true,
};
export type CareActivityActionResourceFormState = {
@@ -74,7 +69,6 @@ export type CareActivityActionFormState = {
export const emptyFlagDefinitionForm = {
name: '',
color: '#f2f2f2',
isEnabled: true,
};
export const emptyPlantFlagForm = {
@@ -94,7 +88,6 @@ export const emptyBulkScheduleForm = {
endsMode: 'after',
endsOn: '',
endsAfterOccurrences: '12',
isEnabled: true,
plantIds: [] as string[],
};
@@ -128,7 +121,6 @@ export function toLocationForm(location: PlantLocation): LocationFormState {
return {
name: location.name,
notes: location.notes ?? '',
isEnabled: location.isEnabled,
};
}
@@ -136,7 +128,6 @@ export function toLocationPayload(form: LocationFormState): PlantLocationPayload
return {
name: form.name.trim(),
notes: form.notes.trim() || null,
isEnabled: form.isEnabled,
};
}
@@ -166,7 +157,6 @@ export function toActionForm(action: CareAction): ActionFormState {
return {
name: action.name,
description: action.description ?? '',
isEnabled: action.isEnabled,
};
}
@@ -174,25 +164,20 @@ export function toActionPayload(form: ActionFormState): CareActionPayload {
return {
name: form.name.trim(),
description: form.description.trim() || null,
isEnabled: form.isEnabled,
};
}
export function toResourceForm(resource: ActionResource): ResourceFormState {
return {
name: resource.name,
category: resource.category ?? '',
notes: resource.notes ?? '',
isEnabled: resource.isEnabled,
};
}
export function toResourcePayload(form: ResourceFormState): ActionResourcePayload {
return {
name: form.name.trim(),
category: form.category.trim() || null,
notes: form.notes.trim() || null,
isEnabled: form.isEnabled,
};
}
@@ -209,7 +194,6 @@ export function toActivityForm(activity: CareActivity): ActivityFormState {
})),
})),
notes: activity.notes ?? '',
isEnabled: activity.isEnabled,
};
}
@@ -226,7 +210,6 @@ export function toActivityPayload(form: ActivityFormState): CareActivityPayload
})),
})),
notes: form.notes.trim() || null,
isEnabled: form.isEnabled,
};
}
@@ -234,7 +217,6 @@ export function toFlagDefinitionForm(flag: PlantFlagDefinition): FlagDefinitionF
return {
name: flag.name,
color: flag.color,
isEnabled: flag.isEnabled,
};
}
@@ -242,7 +224,6 @@ export function toFlagDefinitionPayload(form: FlagDefinitionFormState): PlantFla
return {
name: form.name.trim(),
color: form.color.trim() || null,
isEnabled: form.isEnabled,
};
}
@@ -271,7 +252,6 @@ export function toBulkSchedulePayload(form: BulkScheduleFormState): BulkPlantCar
: form.endsMode === 'after'
? Number(form.endsAfterOccurrences)
: null,
isEnabled: form.isEnabled,
};
}
+19 -42
View File
@@ -24,8 +24,7 @@ namespace plant_manager
string? RepeatOnDays,
string? EndsMode,
DateOnly? EndsOn,
int? EndsAfterOccurrences,
bool IsEnabled);
int? EndsAfterOccurrences);
public record BulkSavePlantCareScheduleRequest(
IReadOnlyList<int> PlantIds,
@@ -38,8 +37,7 @@ namespace plant_manager
string? RepeatOnDays,
string? EndsMode,
DateOnly? EndsOn,
int? EndsAfterOccurrences,
bool IsEnabled);
int? EndsAfterOccurrences);
public record SavePlantTaxonRequest(
string Name,
@@ -51,25 +49,20 @@ namespace plant_manager
public record SavePlantLocationRequest(
string Name,
string? Notes,
bool IsEnabled);
string? Notes);
public record SaveCareActionRequest(
string Name,
string? Description,
bool IsEnabled);
string? Description);
public record SaveActionResourceRequest(
string Name,
string? Category,
string? Notes,
bool IsEnabled);
string? Notes);
public record SaveCareActivityRequest(
string Name,
IReadOnlyList<SaveCareActivityActionRequest> Actions,
string? Notes,
bool IsEnabled);
string? Notes);
public record SaveCareActivityActionRequest(
int CareActionId,
@@ -84,7 +77,6 @@ namespace plant_manager
public record CareActivityActionResourceDto(
int ActionResourceId,
string Name,
string? Category,
decimal? Quantity,
string? Unit,
string? Notes)
@@ -94,7 +86,6 @@ namespace plant_manager
new(
resource.ActionResourceId,
resource.ActionResource.Name,
resource.ActionResource.Category,
resource.Quantity,
resource.Unit,
resource.Notes);
@@ -121,8 +112,7 @@ namespace plant_manager
public record SavePlantFlagDefinitionRequest(
string Name,
string? Color,
bool IsEnabled);
string? Color);
public record AssignPlantFlagRequest(
int PlantFlagDefinitionId,
@@ -183,32 +173,28 @@ namespace plant_manager
public record PlantLocationDto(
int Id,
string Name,
string? Notes,
bool IsEnabled)
string? Notes)
{
public static PlantLocationDto FromLocation(PlantLocation location) =>
new(location.Id, location.Name, location.Notes, location.IsEnabled);
new(location.Id, location.Name, location.Notes);
}
public record CareActionDto(
int Id,
string Name,
string? Description,
bool IsEnabled)
string? Description)
{
public static CareActionDto FromCareAction(CareAction action) =>
new(action.Id, action.Name, action.Description, action.IsEnabled);
new(action.Id, action.Name, action.Description);
}
public record ActionResourceDto(
int Id,
string Name,
string? Category,
string? Notes,
bool IsEnabled)
string? Notes)
{
public static ActionResourceDto FromActionResource(ActionResource resource) =>
new(resource.Id, resource.Name, resource.Category, resource.Notes, resource.IsEnabled);
new(resource.Id, resource.Name, resource.Notes);
}
public record CareActivityDto(
@@ -217,8 +203,7 @@ namespace plant_manager
int CareActionId,
string Action,
IReadOnlyList<CareActivityActionDto> Actions,
string? Notes,
bool IsEnabled)
string? Notes)
{
public static CareActivityDto FromCareActivity(CareActivity activity) =>
new(
@@ -230,8 +215,7 @@ namespace plant_manager
.OrderBy(action => action.SortOrder)
.Select(CareActivityActionDto.FromCareActivityAction)
.ToList(),
activity.Notes,
activity.IsEnabled);
activity.Notes);
}
public record PlantDto(
@@ -257,7 +241,6 @@ namespace plant_manager
today))
.ToList();
var nextCare = schedules
.Where(schedule => schedule.IsEnabled)
.Select(schedule =>
{
var source = plant.CareSchedules.First(item => item.Id == schedule.Id);
@@ -312,8 +295,7 @@ namespace plant_manager
DateOnly? LastPerformedOn,
string LastPerformed,
string NextCare,
string Status,
bool IsEnabled)
string Status)
{
public static PlantCareScheduleDto FromSchedule(
PlantCareSchedule schedule,
@@ -339,8 +321,7 @@ namespace plant_manager
lastPerformedOn,
PlantCareFormatter.FormatRelativeDate(lastPerformedOn, today, "Never"),
PlantCareFormatter.FormatRelativeDate(nextCare, today, "Unscheduled"),
PlantCareFormatter.GetStatus(nextCare, today),
schedule.IsEnabled);
PlantCareFormatter.GetStatus(nextCare, today));
}
private static int GetCompletedOccurrences(PlantCareSchedule schedule) =>
@@ -395,15 +376,13 @@ namespace plant_manager
public record PlantFlagDefinitionDto(
int Id,
string Name,
string Color,
bool IsEnabled)
string Color)
{
public static PlantFlagDefinitionDto FromDefinition(PlantFlagDefinition definition) =>
new(
definition.Id,
definition.Name,
definition.Color,
definition.IsEnabled);
definition.Color);
}
public record PlantFlagDto(
@@ -455,7 +434,6 @@ namespace plant_manager
public record ActionLogResourceDto(
int ActionResourceId,
string Name,
string? Category,
decimal? Quantity,
string? Unit)
{
@@ -463,7 +441,6 @@ namespace plant_manager
new(
resource.ActionResourceId,
resource.ActionResource.Name,
resource.ActionResource.Category,
resource.Quantity,
resource.Unit);
}
@@ -57,7 +57,6 @@ namespace plant_manager.Data
.ValueGeneratedOnAdd();
entity.Property(e => e.Name).HasMaxLength(120).IsRequired();
entity.Property(e => e.Notes).HasMaxLength(1000);
entity.Property(e => e.IsEnabled).IsRequired();
entity.HasIndex(e => e.Name).IsUnique();
});
@@ -68,7 +67,6 @@ namespace plant_manager.Data
.ValueGeneratedOnAdd();
entity.Property(e => e.Name).HasMaxLength(80).IsRequired();
entity.Property(e => e.Description).HasMaxLength(400);
entity.Property(e => e.IsEnabled).IsRequired();
entity.HasIndex(e => e.Name).IsUnique();
});
@@ -78,9 +76,7 @@ namespace plant_manager.Data
entity.Property(e => e.Id)
.ValueGeneratedOnAdd();
entity.Property(e => e.Name).HasMaxLength(120).IsRequired();
entity.Property(e => e.Category).HasMaxLength(80);
entity.Property(e => e.Notes).HasMaxLength(1000);
entity.Property(e => e.IsEnabled).IsRequired();
entity.HasIndex(e => e.Name).IsUnique();
});
@@ -91,7 +87,6 @@ namespace plant_manager.Data
.ValueGeneratedOnAdd();
entity.Property(e => e.Name).HasMaxLength(120).IsRequired();
entity.Property(e => e.Notes).HasMaxLength(1000);
entity.Property(e => e.IsEnabled).IsRequired();
entity.HasIndex(e => e.Name).IsUnique();
});
@@ -174,7 +169,6 @@ namespace plant_manager.Data
entity.Property(e => e.EndsMode).HasMaxLength(20).IsRequired();
entity.Property(e => e.EndsOn);
entity.Property(e => e.EndsAfterOccurrences);
entity.Property(e => e.IsEnabled).IsRequired();
entity.HasIndex(e => new { e.PlantId, e.CareActionId }).IsUnique(false);
entity.HasIndex(e => new { e.PlantId, e.CareActivityId }).IsUnique();
entity.HasOne(e => e.Plant)
@@ -198,7 +192,6 @@ namespace plant_manager.Data
.ValueGeneratedOnAdd();
entity.Property(e => e.Name).HasMaxLength(120).IsRequired();
entity.Property(e => e.Color).HasMaxLength(20).IsRequired();
entity.Property(e => e.IsEnabled).IsRequired();
entity.HasIndex(e => e.Name).IsUnique();
});
+12 -18
View File
@@ -16,14 +16,14 @@ namespace plant_manager.Data
private static readonly ActionResource[] StarterResources =
[
new() { Name = "Water", Category = "Consumable", Notes = "Plain watering resource." },
new() { Name = "Potting Mix", Category = "Medium", Notes = "General purpose houseplant medium." },
new() { Name = "Orchid Bark", Category = "Medium", Notes = "Chunky amendment for airflow and drainage." },
new() { Name = "Perlite", Category = "Medium", Notes = "Lightweight amendment for drainage and aeration." },
new() { Name = "Fertilizer", Category = "Fertilizer", Notes = "General plant nutrient." },
new() { Name = "Nursery Pot", Category = "Container", Notes = "Basic plastic grow pot." },
new() { Name = "Neem Oil", Category = "Treatment", Notes = "Common pest treatment." },
new() { Name = "Pruners", Category = "Equipment", Notes = "Cutting tool for pruning or cleanup." }
new() { Name = "Water", Notes = "Plain watering resource." },
new() { Name = "Potting Mix", Notes = "General purpose houseplant medium." },
new() { Name = "Orchid Bark", Notes = "Chunky amendment for airflow and drainage." },
new() { Name = "Perlite", Notes = "Lightweight amendment for drainage and aeration." },
new() { Name = "Fertilizer", Notes = "General plant nutrient." },
new() { Name = "Nursery Pot", Notes = "Basic plastic grow pot." },
new() { Name = "Neem Oil", Notes = "Common pest treatment." },
new() { Name = "Pruners", Notes = "Cutting tool for pruning or cleanup." }
];
private static readonly StarterCareActivity[] StarterCareActivities =
@@ -117,8 +117,7 @@ namespace plant_manager.Data
.Select(starterAction => new CareAction
{
Name = starterAction.Name,
Description = starterAction.Description,
IsEnabled = starterAction.IsEnabled
Description = starterAction.Description
})
.ToList();
@@ -143,9 +142,7 @@ namespace plant_manager.Data
.Select(starterResource => new ActionResource
{
Name = starterResource.Name,
Category = starterResource.Category,
Notes = starterResource.Notes,
IsEnabled = starterResource.IsEnabled
Notes = starterResource.Notes
})
.ToList();
@@ -201,7 +198,6 @@ namespace plant_manager.Data
return new CareActivity
{
Name = starterActivity.Name,
IsEnabled = true,
Actions = actions
};
})
@@ -229,8 +225,7 @@ namespace plant_manager.Data
.Select(starterFlag => new PlantFlagDefinition
{
Name = starterFlag.Name,
Color = starterFlag.Color,
IsEnabled = starterFlag.IsEnabled
Color = starterFlag.Color
})
.ToList();
@@ -347,8 +342,7 @@ namespace plant_manager.Data
string.Equals(existingName, starterLocation, StringComparison.OrdinalIgnoreCase)))
.Select(starterLocation => new PlantLocation
{
Name = starterLocation,
IsEnabled = true
Name = starterLocation
})
.ToList();
@@ -86,14 +86,6 @@ namespace plant_manager.Data.Migrations
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Category")
.HasMaxLength(80)
.HasColumnType("TEXT");
b.Property<bool>("IsEnabled")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(120)
@@ -120,10 +112,6 @@ namespace plant_manager.Data.Migrations
b.Property<string>("Description")
.HasMaxLength(400)
.HasColumnType("TEXT");
b.Property<bool>("IsEnabled")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(80)
@@ -142,10 +130,6 @@ namespace plant_manager.Data.Migrations
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("IsEnabled")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(120)
@@ -265,10 +249,6 @@ namespace plant_manager.Data.Migrations
b.Property<int>("EveryDays")
.HasColumnType("INTEGER");
b.Property<bool>("IsEnabled")
.HasColumnType("INTEGER");
b.Property<int>("PlantId")
.HasColumnType("INTEGER");
@@ -347,10 +327,6 @@ namespace plant_manager.Data.Migrations
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<bool>("IsEnabled")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(120)
@@ -369,10 +345,6 @@ namespace plant_manager.Data.Migrations
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("IsEnabled")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(120)
@@ -18,9 +18,7 @@ namespace plant_manager.Data.Migrations
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", maxLength: 120, nullable: false),
Category = table.Column<string>(type: "TEXT", maxLength: 80, nullable: true),
Notes = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true),
IsEnabled = table.Column<bool>(type: "INTEGER", nullable: false)
Notes = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true)
},
constraints: table =>
{
@@ -34,8 +32,7 @@ namespace plant_manager.Data.Migrations
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", maxLength: 80, nullable: false),
Description = table.Column<string>(type: "TEXT", maxLength: 400, nullable: true),
IsEnabled = table.Column<bool>(type: "INTEGER", nullable: false)
Description = table.Column<string>(type: "TEXT", maxLength: 400, nullable: true)
},
constraints: table =>
{
@@ -49,8 +46,7 @@ namespace plant_manager.Data.Migrations
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", maxLength: 120, nullable: false),
Notes = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true),
IsEnabled = table.Column<bool>(type: "INTEGER", nullable: false)
Notes = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true)
},
constraints: table =>
{
@@ -64,8 +60,7 @@ namespace plant_manager.Data.Migrations
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", maxLength: 120, nullable: false),
Color = table.Column<string>(type: "TEXT", maxLength: 20, nullable: false),
IsEnabled = table.Column<bool>(type: "INTEGER", nullable: false)
Color = table.Column<string>(type: "TEXT", maxLength: 20, nullable: false)
},
constraints: table =>
{
@@ -79,8 +74,7 @@ namespace plant_manager.Data.Migrations
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", maxLength: 120, nullable: false),
Notes = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true),
IsEnabled = table.Column<bool>(type: "INTEGER", nullable: false)
Notes = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true)
},
constraints: table =>
{
@@ -238,8 +232,7 @@ namespace plant_manager.Data.Migrations
RepeatOnDays = table.Column<string>(type: "TEXT", maxLength: 40, nullable: true),
EndsMode = table.Column<string>(type: "TEXT", maxLength: 20, nullable: false),
EndsOn = table.Column<DateOnly>(type: "TEXT", nullable: true),
EndsAfterOccurrences = table.Column<int>(type: "INTEGER", nullable: true),
IsEnabled = table.Column<bool>(type: "INTEGER", nullable: false)
EndsAfterOccurrences = table.Column<int>(type: "INTEGER", nullable: true)
},
constraints: table =>
{
@@ -83,14 +83,6 @@ namespace plant_manager.Data.Migrations
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Category")
.HasMaxLength(80)
.HasColumnType("TEXT");
b.Property<bool>("IsEnabled")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(120)
@@ -117,10 +109,6 @@ namespace plant_manager.Data.Migrations
b.Property<string>("Description")
.HasMaxLength(400)
.HasColumnType("TEXT");
b.Property<bool>("IsEnabled")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(80)
@@ -139,10 +127,6 @@ namespace plant_manager.Data.Migrations
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("IsEnabled")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(120)
@@ -262,10 +246,6 @@ namespace plant_manager.Data.Migrations
b.Property<int>("EveryDays")
.HasColumnType("INTEGER");
b.Property<bool>("IsEnabled")
.HasColumnType("INTEGER");
b.Property<int>("PlantId")
.HasColumnType("INTEGER");
@@ -344,10 +324,6 @@ namespace plant_manager.Data.Migrations
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<bool>("IsEnabled")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(120)
@@ -366,10 +342,6 @@ namespace plant_manager.Data.Migrations
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("IsEnabled")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(120)
@@ -5,9 +5,7 @@ namespace plant_manager.Data.Models
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Category { get; set; }
public string? Notes { get; set; }
public bool IsEnabled { get; set; } = true;
public List<CareActivityActionResource> CareActivityActionResources { get; set; } = [];
public List<ActionLogResource> ActionLogResources { get; set; } = [];
-1
View File
@@ -6,7 +6,6 @@ namespace plant_manager.Data.Models
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public bool IsEnabled { get; set; } = true;
public List<CareActivityAction> CareActivityActions { get; set; } = [];
public List<ActionLog> ActionLogs { get; set; } = [];
@@ -6,7 +6,6 @@ namespace plant_manager.Data.Models
public string Name { get; set; } = string.Empty;
public string? Notes { get; set; }
public bool IsEnabled { get; set; } = true;
public List<CareActivityAction> Actions { get; set; } = [];
public List<ActionLog> ActionLogs { get; set; } = [];
@@ -15,7 +15,6 @@ namespace plant_manager.Data.Models
public string EndsMode { get; set; } = "after";
public DateOnly? EndsOn { get; set; }
public int? EndsAfterOccurrences { get; set; }
public bool IsEnabled { get; set; } = true;
public Plant Plant { get; set; } = null!;
public CareAction CareAction { get; set; } = null!;
@@ -5,7 +5,6 @@ namespace plant_manager.Data.Models
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Color { get; set; } = "#f2f2f2";
public bool IsEnabled { get; set; } = true;
public List<PlantFlag> PlantFlags { get; set; } = [];
}
@@ -6,7 +6,6 @@ namespace plant_manager.Data.Models
public string Name { get; set; } = string.Empty;
public string? Notes { get; set; }
public bool IsEnabled { get; set; } = true;
public List<Plant> Plants { get; set; } = [];
}
@@ -43,9 +43,9 @@ namespace plant_manager.Endpoints
}
var primaryAction = activity.PrimaryAction();
if (!activity.IsEnabled || primaryAction is null || activity.Actions.Any(action => !action.CareAction.IsEnabled))
if (primaryAction is null)
{
return Results.BadRequest(new { error = "Disabled activities cannot be logged." });
return Results.BadRequest(new { error = "Care activity has no configured actions." });
}
var performedOn = request.PerformedOn ?? DateOnly.FromDateTime(DateTime.UtcNow);
@@ -107,9 +107,9 @@ namespace plant_manager.Endpoints
}
var primaryAction = activity.PrimaryAction();
if (!activity.IsEnabled || primaryAction is null || activity.Actions.Any(action => !action.CareAction.IsEnabled))
if (primaryAction is null)
{
return Results.BadRequest(new { error = "Disabled activities cannot be logged." });
return Results.BadRequest(new { error = "Care activity has no configured actions." });
}
var (resources, resourceError) = await BuildLogResources(request.Resources, activity, db);
@@ -188,11 +188,6 @@ namespace plant_manager.Endpoints
return ([], "One or more resources were not found.");
}
if (resourcesById.Values.Any(resource => !resource.IsEnabled))
{
return ([], "Disabled resources cannot be logged.");
}
return (requestedResources
.Select(resource => new ActionLogResource
{
@@ -11,9 +11,7 @@ namespace plant_manager.Endpoints
app.MapGet("/api/action-resources", async (ApplicationDbContext db) =>
{
var resources = await db.ActionResources
.OrderByDescending(resource => resource.IsEnabled)
.ThenBy(resource => resource.Category)
.ThenBy(resource => resource.Name)
.OrderBy(resource => resource.Name)
.Select(resource => ActionResourceDto.FromActionResource(resource))
.ToListAsync();
@@ -37,9 +35,7 @@ namespace plant_manager.Endpoints
var resource = new ActionResource
{
Name = name,
Category = string.IsNullOrWhiteSpace(request.Category) ? null : request.Category.Trim(),
Notes = string.IsNullOrWhiteSpace(request.Notes) ? null : request.Notes.Trim(),
IsEnabled = request.IsEnabled
Notes = string.IsNullOrWhiteSpace(request.Notes) ? null : request.Notes.Trim()
};
db.ActionResources.Add(resource);
@@ -70,9 +66,7 @@ namespace plant_manager.Endpoints
}
resource.Name = name;
resource.Category = string.IsNullOrWhiteSpace(request.Category) ? null : request.Category.Trim();
resource.Notes = string.IsNullOrWhiteSpace(request.Notes) ? null : request.Notes.Trim();
resource.IsEnabled = request.IsEnabled;
await db.SaveChangesAsync();
@@ -91,9 +85,7 @@ namespace plant_manager.Endpoints
|| await db.CareActivityActionResources.AnyAsync(activityResource => activityResource.ActionResourceId == id);
if (isInUse)
{
resource.IsEnabled = false;
await db.SaveChangesAsync();
return Results.Conflict(new { error = "Resource is in use, so it was disabled instead of deleted." });
return Results.Conflict(new { error = "Resource is in use." });
}
db.ActionResources.Remove(resource);
@@ -11,8 +11,7 @@ namespace plant_manager.Endpoints
app.MapGet("/api/care-actions", async (ApplicationDbContext db) =>
{
var actions = await db.CareActions
.OrderByDescending(action => action.IsEnabled)
.ThenBy(action => action.Name)
.OrderBy(action => action.Name)
.Select(action => CareActionDto.FromCareAction(action))
.ToListAsync();
@@ -36,8 +35,7 @@ namespace plant_manager.Endpoints
var action = new CareAction
{
Name = name,
Description = string.IsNullOrWhiteSpace(request.Description) ? null : request.Description.Trim(),
IsEnabled = request.IsEnabled
Description = string.IsNullOrWhiteSpace(request.Description) ? null : request.Description.Trim()
};
db.CareActions.Add(action);
@@ -69,7 +67,6 @@ namespace plant_manager.Endpoints
action.Name = name;
action.Description = string.IsNullOrWhiteSpace(request.Description) ? null : request.Description.Trim();
action.IsEnabled = request.IsEnabled;
await db.SaveChangesAsync();
@@ -87,9 +84,7 @@ namespace plant_manager.Endpoints
var hasLogs = await db.ActionLogs.AnyAsync(log => log.CareActionId == id);
if (hasLogs)
{
action.IsEnabled = false;
await db.SaveChangesAsync();
return Results.Conflict(new { error = "Action has care history, so it was disabled instead of deleted." });
return Results.Conflict(new { error = "Action has care history." });
}
db.CareActions.Remove(action);
@@ -16,8 +16,7 @@ namespace plant_manager.Endpoints
.Include(activity => activity.Actions)
.ThenInclude(action => action.Resources)
.ThenInclude(resource => resource.ActionResource)
.OrderByDescending(activity => activity.IsEnabled)
.ThenBy(activity => activity.Name)
.OrderBy(activity => activity.Name)
.Select(activity => CareActivityDto.FromCareActivity(activity))
.ToListAsync();
@@ -43,7 +42,6 @@ namespace plant_manager.Endpoints
{
Name = name,
Notes = string.IsNullOrWhiteSpace(request.Notes) ? null : request.Notes.Trim(),
IsEnabled = request.IsEnabled,
Actions = validation.Actions
};
@@ -83,7 +81,6 @@ namespace plant_manager.Endpoints
activity.Name = name;
activity.Notes = string.IsNullOrWhiteSpace(request.Notes) ? null : request.Notes.Trim();
activity.IsEnabled = request.IsEnabled;
db.CareActivityActions.RemoveRange(activity.Actions);
activity.Actions = validation.Actions;
@@ -104,9 +101,7 @@ namespace plant_manager.Endpoints
|| await db.PlantCareSchedules.AnyAsync(schedule => schedule.CareActivityId == id);
if (hasHistory)
{
activity.IsEnabled = false;
await db.SaveChangesAsync();
return Results.Conflict(new { error = "Activity is in use, so it was disabled instead of deleted." });
return Results.Conflict(new { error = "Activity is in use." });
}
db.CareActivities.Remove(activity);
@@ -150,11 +145,6 @@ namespace plant_manager.Endpoints
return ([], "One or more care actions were not found.");
}
if (actionsById.Values.Any(action => !action.IsEnabled))
{
return ([], "Disabled actions cannot be used in activities.");
}
var requestedResources = requestedActions
.SelectMany(action => action.Resources ?? [])
.GroupBy(resource => resource.ActionResourceId)
@@ -181,11 +171,6 @@ namespace plant_manager.Endpoints
return ([], "One or more resources were not found.");
}
if (resourcesById.Values.Any(resource => !resource.IsEnabled))
{
return ([], "Disabled resources cannot be used in activities.");
}
var activityActions = requestedActions
.Select((actionRequest, index) => new CareActivityAction
{
+4 -14
View File
@@ -21,10 +21,6 @@ namespace plant_manager.Endpoints
.ThenInclude(activity => activity.Actions)
.ThenInclude(action => action.Resources)
.ThenInclude(resource => resource.ActionResource)
.Where(schedule =>
schedule.IsEnabled
&& schedule.CareActivity.Actions.All(action => action.CareAction.IsEnabled)
&& schedule.CareActivity.IsEnabled)
.OrderBy(schedule => schedule.Plant.Nickname)
.ThenBy(schedule => schedule.CareActivity.Name)
.ToListAsync();
@@ -86,17 +82,16 @@ namespace plant_manager.Endpoints
}
var primaryAction = activity.PrimaryAction();
if (!activity.IsEnabled || primaryAction is null || activity.Actions.Any(action => !action.CareAction.IsEnabled))
if (primaryAction is null)
{
return Results.BadRequest(new { error = "Disabled activities cannot be logged." });
return Results.BadRequest(new { error = "Care activity has no configured actions." });
}
var today = DateOnly.FromDateTime(DateTime.UtcNow);
var schedules = await db.PlantCareSchedules
.Include(schedule => schedule.Plant)
.Where(schedule =>
schedule.IsEnabled
&& schedule.CareActivityId == activity.Id
schedule.CareActivityId == activity.Id
&& plantIds.Contains(schedule.PlantId))
.ToListAsync();
var schedulePlantIds = schedules
@@ -104,7 +99,7 @@ namespace plant_manager.Endpoints
.ToHashSet();
if (schedulePlantIds.Count != plantIds.Count)
{
return Results.BadRequest(new { error = "One or more plants do not have this enabled schedule." });
return Results.BadRequest(new { error = "One or more plants do not have this schedule." });
}
var latestLogs = await db.ActionLogs
@@ -169,11 +164,6 @@ namespace plant_manager.Endpoints
return Results.BadRequest(new { error = "One or more resources were not found." });
}
if (resourcesById.Values.Any(resource => !resource.IsEnabled))
{
return Results.BadRequest(new { error = "Disabled resources cannot be logged." });
}
var performedOn = request.PerformedOn ?? today;
var notes = string.IsNullOrWhiteSpace(request.Notes) ? null : request.Notes.Trim();
var logs = schedules
@@ -31,9 +31,9 @@ namespace plant_manager.Endpoints
}
var primaryAction = activity.PrimaryAction();
if (!activity.IsEnabled || primaryAction is null || activity.Actions.Any(action => !action.CareAction.IsEnabled))
if (primaryAction is null)
{
return Results.BadRequest(new { error = "Disabled activities cannot be scheduled." });
return Results.BadRequest(new { error = "Care activity has no configured actions." });
}
var plants = await db.Plants
@@ -73,7 +73,6 @@ namespace plant_manager.Endpoints
schedule.CareActionId = primaryAction.Id;
schedule.CareActivityId = activity.Id;
ApplyRecurrence(schedule, recurrence);
schedule.IsEnabled = request.IsEnabled;
}
await db.SaveChangesAsync();
+3 -7
View File
@@ -206,8 +206,7 @@ namespace plant_manager.Endpoints
null,
"after",
null,
12,
true)
12)
];
}
@@ -234,11 +233,9 @@ namespace plant_manager.Endpoints
}
if (activitiesById.Values.Any(activity =>
!activity.IsEnabled
|| activity.PrimaryAction() is null
|| activity.Actions.Any(action => !action.CareAction.IsEnabled)))
activity.PrimaryAction() is null))
{
return "Disabled activities cannot be scheduled.";
return "Care activities must have at least one action.";
}
var requestedActivityIds = activityIds.ToHashSet();
@@ -280,7 +277,6 @@ namespace plant_manager.Endpoints
requestedSchedule.EndsAfterOccurrences,
requestedSchedule.ScheduledFor,
requestedSchedule.EveryDays));
schedule.IsEnabled = requestedSchedule.IsEnabled;
}
return null;
+5 -10
View File
@@ -11,8 +11,7 @@ namespace plant_manager.Endpoints
app.MapGet("/api/plant-flags", async (ApplicationDbContext db) =>
{
var definitions = await db.PlantFlagDefinitions
.OrderByDescending(definition => definition.IsEnabled)
.ThenBy(definition => definition.Name)
.OrderBy(definition => definition.Name)
.Select(definition => PlantFlagDefinitionDto.FromDefinition(definition))
.ToListAsync();
@@ -37,8 +36,7 @@ namespace plant_manager.Endpoints
var definition = new PlantFlagDefinition
{
Name = name,
Color = NormalizeColor(request.Color),
IsEnabled = request.IsEnabled
Color = NormalizeColor(request.Color)
};
db.PlantFlagDefinitions.Add(definition);
@@ -70,7 +68,6 @@ namespace plant_manager.Endpoints
definition.Name = name;
definition.Color = NormalizeColor(request.Color);
definition.IsEnabled = request.IsEnabled;
await db.SaveChangesAsync();
@@ -88,9 +85,7 @@ namespace plant_manager.Endpoints
var isUsed = await db.PlantFlags.AnyAsync(flag => flag.PlantFlagDefinitionId == id);
if (isUsed)
{
definition.IsEnabled = false;
await db.SaveChangesAsync();
return Results.Conflict(new { error = "Flag is assigned to plants, so it was disabled instead of deleted." });
return Results.Conflict(new { error = "Flag is assigned to plants." });
}
db.PlantFlagDefinitions.Remove(definition);
@@ -111,9 +106,9 @@ namespace plant_manager.Endpoints
}
var definition = await db.PlantFlagDefinitions.FindAsync(request.PlantFlagDefinitionId);
if (definition is null || !definition.IsEnabled)
if (definition is null)
{
return Results.BadRequest(new { error = "Flag was not found or is disabled." });
return Results.BadRequest(new { error = "Flag was not found." });
}
var hasActiveFlag = await db.PlantFlags.AnyAsync(flag =>
@@ -11,8 +11,7 @@ namespace plant_manager.Endpoints
app.MapGet("/api/plant-locations", async (ApplicationDbContext db) =>
{
var locations = await db.PlantLocations
.OrderByDescending(location => location.IsEnabled)
.ThenBy(location => location.Name)
.OrderBy(location => location.Name)
.Select(location => PlantLocationDto.FromLocation(location))
.ToListAsync();
@@ -36,8 +35,7 @@ namespace plant_manager.Endpoints
var location = new PlantLocation
{
Name = name,
Notes = string.IsNullOrWhiteSpace(request.Notes) ? null : request.Notes.Trim(),
IsEnabled = request.IsEnabled
Notes = string.IsNullOrWhiteSpace(request.Notes) ? null : request.Notes.Trim()
};
db.PlantLocations.Add(location);
@@ -69,7 +67,6 @@ namespace plant_manager.Endpoints
location.Name = name;
location.Notes = string.IsNullOrWhiteSpace(request.Notes) ? null : request.Notes.Trim();
location.IsEnabled = request.IsEnabled;
await db.SaveChangesAsync();
@@ -87,9 +84,7 @@ namespace plant_manager.Endpoints
var isInUse = await db.Plants.AnyAsync(plant => plant.LocationId == id);
if (isInUse)
{
location.IsEnabled = false;
await db.SaveChangesAsync();
return Results.Conflict(new { error = "Location is in use, so it was disabled instead of deleted." });
return Results.Conflict(new { error = "Location is assigned to one or more plants." });
}
db.PlantLocations.Remove(location);