Skip to content

Commit 3ec6803

Browse files
author
gfer
committed
UI: layout, подсказки по ролям, ограничения для не залогиненных
- Overview: grid вместо flex для Feed+Chart (стабильный layout при логине) - FeedCard: подсказка feed.adminOnly вместо passwordRequired - ProtectedRoute: protected.passwordRequired для Settings/System/Library - Ограничения: PDF, экспорт, корм — только для залогиненных - FoodManagement: чекбоксы только для admin Made-with: Cursor
1 parent 22106a9 commit 3ec6803

7 files changed

Lines changed: 93 additions & 52 deletions

File tree

app/ui/locales/en.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
{
2+
"protected": {
3+
"passwordRequired": "Enter admin password to access this section."
4+
},
25
"nav": {
36
"dashboard": "Dashboard",
47
"timeline": "Timeline",
@@ -341,6 +344,7 @@
341344
"mergeSpeciesFailed": "Failed to merge duplicate species"
342345
},
343346
"common": {
347+
"loginRequiredForExport": "Sign in to export data and reports",
344348
"retry": "Retry",
345349
"cancel": "Cancel",
346350
"noData": "No data available",
@@ -430,7 +434,8 @@
430434
"feedDispensed": "Feed dispensed",
431435
"lastDispense": "Last dispense",
432436
"donate": "Donate",
433-
"donatePlaceholder": "Donate (set URL in Settings)"
437+
"donatePlaceholder": "Donate (set URL in Settings)",
438+
"adminOnly": "Button available to admin. Volunteers can help with species — contact us."
434439
},
435440
"speciesSummary": {
436441
"totalDetectionStats": "Total Detection Stats",
@@ -490,7 +495,8 @@
490495
"food": "Food",
491496
"description": "Description",
492497
"active": "Active",
493-
"noDescription": "No description available"
498+
"noDescription": "No description available",
499+
"loginRequired": "Sign in to change food in the feeder"
494500
},
495501
"status": {
496502
"motion": "Motion",

app/ui/locales/ru.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
{
2+
"protected": {
3+
"passwordRequired": "Введите пароль администратора для доступа к этому разделу."
4+
},
25
"nav": {
36
"dashboard": "Панель",
47
"timeline": "Записи",
@@ -341,6 +344,7 @@
341344
"mergeSpeciesFailed": "Ошибка объединения дубликатов видов"
342345
},
343346
"common": {
347+
"loginRequiredForExport": "Войдите для экспорта и отчётов",
344348
"retry": "Повторить",
345349
"cancel": "Отмена",
346350
"noData": "Нет данных",
@@ -430,7 +434,8 @@
430434
"feedDispensed": "Корм выдан",
431435
"lastDispense": "Последняя выдача",
432436
"donate": "Пожертвовать",
433-
"donatePlaceholder": "Пожертвовать (укажите ссылку в Настройках)"
437+
"donatePlaceholder": "Пожертвовать (укажите ссылку в Настройках)",
438+
"adminOnly": "Кнопка доступна администратору. Волонтёры могут помочь с видами — свяжитесь с нами."
434439
},
435440
"speciesSummary": {
436441
"totalDetectionStats": "Статистика обнаружений",
@@ -490,7 +495,8 @@
490495
"food": "Корм",
491496
"description": "Описание",
492497
"active": "Активен",
493-
"noDescription": "Описание отсутствует"
498+
"noDescription": "Описание отсутствует",
499+
"loginRequired": "Войдите для изменения состава корма в кормушке"
494500
},
495501
"status": {
496502
"motion": "Триггер",

app/ui/src/components/FeedCard.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,7 @@ export const FeedCard = () => {
6464
<Typography variant="h6">{t('feed.feederControl')}</Typography>
6565
{!isAdmin && (
6666
<Typography variant="body2" color="text.secondary">
67-
{t('unknowns.passwordRequired')}{' '}
68-
<Link to="/settings" style={{ fontWeight: 600 }}>
69-
{t('nav.settings')}
70-
</Link>
67+
{t('feed.adminOnly')}
7168
</Typography>
7269
)}
7370
<Button

app/ui/src/components/ProtectedRoute.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export function ProtectedRoute({ children, title }: ProtectedRouteProps) {
5858
{title}
5959
</Typography>
6060
<Typography color="text.secondary" sx={{ mb: 3 }}>
61-
{t('settings.passwordRequired')}
61+
{t('protected.passwordRequired')}
6262
</Typography>
6363
<SettingsPasswordDialog
6464
open

app/ui/src/pages/FoodManagement/index.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,23 @@ import TableHead from '@mui/material/TableHead';
88
import TableRow from '@mui/material/TableRow';
99
import Paper from '@mui/material/Paper';
1010
import Box from '@mui/material/Box';
11+
import Alert from '@mui/material/Alert';
1112
import CircularProgress from '@mui/material/CircularProgress';
1213
import Typography from '@mui/material/Typography';
1314
import Checkbox from '@mui/material/Checkbox';
1415
import Avatar from '@mui/material/Avatar';
1516
import Info from '@mui/icons-material/Info';
17+
import Tooltip from '@mui/material/Tooltip';
1618
import { resolveImageUrl, fetchBirdFood, toggleBirdFood } from '../../api/api';
1719
import { BirdFood } from '../../types';
1820
import { PageHelp } from '../../components/PageHelp';
1921
import { foodHelpConfig } from '../../page-help-config';
22+
import { useProtectedArea } from '../../contexts/ProtectedAreaContext';
2023

2124
export const FoodManagement = () => {
2225
const { t } = useTranslation();
2326
const queryClient = useQueryClient();
27+
const { isAdmin } = useProtectedArea();
2428
const { data: foodData, isLoading } = useQuery({
2529
queryKey: ['birdFood'],
2630
queryFn: fetchBirdFood,
@@ -59,6 +63,11 @@ export const FoodManagement = () => {
5963
return (
6064
<Box mb={4}>
6165
<PageHelp {...foodHelpConfig} />
66+
{!isAdmin && (
67+
<Alert severity="info" sx={{ mb: 2 }}>
68+
{t('food.loginRequired')}
69+
</Alert>
70+
)}
6271
<TableContainer component={Paper}>
6372
<Table>
6473
<TableHead>
@@ -94,11 +103,16 @@ export const FoodManagement = () => {
94103
</Typography>
95104
</TableCell>
96105
<TableCell align="center" sx={{ width: '100px' }}>
97-
<Checkbox
98-
checked={food.active}
99-
onChange={() => toggleMutation.mutate(food.id)}
100-
color="primary"
101-
/>
106+
<Tooltip title={!isAdmin ? t('food.loginRequired') : ''}>
107+
<span>
108+
<Checkbox
109+
checked={food.active}
110+
onChange={() => isAdmin && toggleMutation.mutate(food.id)}
111+
color="primary"
112+
disabled={!isAdmin}
113+
/>
114+
</span>
115+
</Tooltip>
102116
</TableCell>
103117
</TableRow>
104118
))}

app/ui/src/pages/Overview/index.tsx

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import VideocamOutlined from '@mui/icons-material/VideocamOutlined';
2727
import { BirdIcon } from '../../components/icons/BirdIcon';
2828
import { PageHelp } from '../../components/PageHelp';
2929
import { overviewHelpConfig } from '../../page-help-config';
30+
import { useProtectedArea } from '../../contexts/ProtectedAreaContext';
31+
import Tooltip from '@mui/material/Tooltip';
3032

3133
const formatHour = (hour: number) => {
3234
const date = new Date();
@@ -36,6 +38,7 @@ const formatHour = (hour: number) => {
3638

3739
export const Overview = () => {
3840
const { t } = useTranslation();
41+
const { canEdit } = useProtectedArea();
3942
const [selectedDay, setSelectedDay] = useState<Dayjs>(dayjs());
4043
const [downloadingPdf, setDownloadingPdf] = useState(false);
4144

@@ -114,24 +117,29 @@ export const Overview = () => {
114117
format="YYYY-MM-DD"
115118
/>
116119
</LocalizationProvider>
117-
<Button
118-
variant="outlined"
119-
size="medium"
120-
startIcon={<DownloadIcon />}
121-
disabled={downloadingPdf}
122-
onClick={async () => {
123-
setDownloadingPdf(true);
124-
try {
125-
await downloadReportPdf(selectedDay.format('YYYY-MM'));
126-
} catch (err) {
127-
console.error('PDF download failed:', err);
128-
} finally {
129-
setDownloadingPdf(false);
130-
}
131-
}}
132-
>
133-
{downloadingPdf ? '...' : t('overview.downloadPdf')}
134-
</Button>
120+
<Tooltip title={!canEdit ? t('common.loginRequiredForExport') : undefined}>
121+
<span>
122+
<Button
123+
variant="outlined"
124+
size="medium"
125+
startIcon={<DownloadIcon />}
126+
disabled={downloadingPdf || !canEdit}
127+
onClick={async () => {
128+
if (!canEdit) return;
129+
setDownloadingPdf(true);
130+
try {
131+
await downloadReportPdf(selectedDay.format('YYYY-MM'));
132+
} catch (err) {
133+
console.error('PDF download failed:', err);
134+
} finally {
135+
setDownloadingPdf(false);
136+
}
137+
}}
138+
>
139+
{downloadingPdf ? '...' : t('overview.downloadPdf')}
140+
</Button>
141+
</span>
142+
</Tooltip>
135143
</Box>
136144
</Grid>
137145
</Grid>
@@ -254,25 +262,33 @@ export const Overview = () => {
254262
{weather && <WeatherCard weather={weather} />}
255263
</Grid>
256264

257-
{/* Feed Control */}
258-
<Grid size={{ xs: 12, sm: 6, md: 4 }} sx={{ display: 'flex' }}>
259-
<FeedCard />
260-
</Grid>
261-
262-
{/* Hourly Activity Line Chart */}
263-
<Grid size={{ xs: 12, md: 8 }} sx={{ display: 'flex' }}>
264-
<Paper sx={{ p: 2, width: '100%' }}>
265-
<Typography variant="h6" gutterBottom>
266-
{t('overview.hourlyActivity')}
267-
</Typography>
268-
{overviewData?.topSpecies && overviewData.topSpecies.length > 0 ? (
269-
<HourlyActivityChart data={overviewData.topSpecies} />
270-
) : (
271-
<Typography color="text.secondary" sx={{ py: 4 }}>
272-
{t('overview.noData')}
265+
{/* Feed Control + Hourly Activity — в одной строке без зазора */}
266+
<Grid size={12}>
267+
<Box
268+
sx={{
269+
display: 'grid',
270+
gridTemplateColumns: { xs: '1fr', sm: 'minmax(280px, 320px) 1fr' },
271+
gap: 2,
272+
alignItems: 'stretch',
273+
width: '100%',
274+
}}
275+
>
276+
<Box>
277+
<FeedCard />
278+
</Box>
279+
<Paper sx={{ p: 2, minWidth: 0 }}>
280+
<Typography variant="h6" gutterBottom>
281+
{t('overview.hourlyActivity')}
273282
</Typography>
274-
)}
275-
</Paper>
283+
{overviewData?.topSpecies && overviewData.topSpecies.length > 0 ? (
284+
<HourlyActivityChart data={overviewData.topSpecies} />
285+
) : (
286+
<Typography color="text.secondary" sx={{ py: 4 }}>
287+
{t('overview.noData')}
288+
</Typography>
289+
)}
290+
</Paper>
291+
</Box>
276292
</Grid>
277293
</Grid>
278294

app/ui/src/pages/Timeline/index.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import ListItemText from '@mui/material/ListItemText';
3131
import { PageHelp } from '../../components/PageHelp';
3232
import { timelineHelpConfig } from '../../page-help-config';
3333
import { getTimeRange, type TimeOfDay } from '../../utils/timeUtils';
34+
import { useProtectedArea } from '../../contexts/ProtectedAreaContext';
3435

3536
function useSpeciesList(visits: SpeciesVisit[] | undefined) {
3637
return visits
@@ -56,6 +57,7 @@ function useFilteredVisits(
5657

5758
export function TimelinePage() {
5859
const { t } = useTranslation();
60+
const { canEdit } = useProtectedArea();
5961
const [searchParams] = useSearchParams();
6062
const [selectedSpeciesIds, setSelectedSpeciesIds] = useState<number[]>([]);
6163
const [timeOfDay, setTimeOfDay] = useState<TimeOfDay>('all');
@@ -233,11 +235,11 @@ export function TimelinePage() {
233235
))}
234236
</Select>
235237
</FormControl>
236-
<Tooltip title={t('timeline.export')}>
238+
<Tooltip title={!canEdit ? t('common.loginRequiredForExport') : t('timeline.export')}>
237239
<span>
238240
<IconButton
239241
onClick={(e) => setExportAnchor(e.currentTarget)}
240-
disabled={exporting}
242+
disabled={exporting || !canEdit}
241243
aria-label={t('timeline.export')}
242244
>
243245
<DownloadIcon />

0 commit comments

Comments
 (0)