remove enabled state
This commit is contained in:
@@ -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
@@ -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)}>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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; } = [];
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user