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