diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index d6d5d3c..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: "[BUG]" -labels: '' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] - -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..e1318ee --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,108 @@ +name: Bug Report +description: Create a report to help us improve TREK +title: "[BUG] " +labels: [] +body: + - type: checkboxes + id: preflight + attributes: + label: Pre-flight checklist + options: + - label: I have searched [existing issues](https://github.com/mauriceboe/TREK/issues) and this bug has not been reported yet + required: true + - label: I am running the latest available version of TREK + required: true + + - type: input + id: version + attributes: + label: TREK version + description: Found in the Settings → About, or in the Docker image tag + placeholder: "e.g. 2.8.0" + validations: + required: true + + - type: textarea + id: description + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is. + placeholder: When I do X, Y happens instead of Z… + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: Step-by-step instructions to reliably trigger the bug. + placeholder: | + 1. Go to '...' + 2. Click on '...' + 3. See error + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected behavior + description: What did you expect to happen? + validations: + required: true + + - type: dropdown + id: deployment + attributes: + label: Deployment method + options: + - Docker Compose + - Docker (standalone) + - Kubernetes / Helm + - Unraid template + - Sources + - Other + validations: + required: true + + - type: input + id: os + attributes: + label: Host OS + placeholder: "e.g. Ubuntu 24.04, Unraid 6.12, Synology DSM 7" + + - type: dropdown + id: user_os + attributes: + label: Accessing TREK from + options: + - Desktop browser + - Mobile browser + - Mobile app (PWA) + validations: + required: true + + - type: input + id: browser + attributes: + label: Browser (if applicable) + placeholder: "e.g. Chrome 124, Firefox 125, Safari 17" + + - type: textarea + id: logs + attributes: + label: Relevant logs or error output + description: Paste any relevant server or browser console output here. + render: shell + + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: Drag and drop screenshots here if applicable. + + - type: textarea + id: context + attributes: + label: Additional context + description: Anything else that might help us understand the issue. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..a1e0f7b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Documentation + url: https://github.com/mauriceboe/TREK/wiki + about: Check the docs before opening an issue + - name: Feature Request + url: https://github.com/mauriceboe/TREK/discussions/new?category=feature-requests + about: Suggest a new feature or improvement in Discussions + - name: Questions & Help + url: https://github.com/mauriceboe/TREK/discussions + about: For questions and general help, use Discussions instead \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index e0c0168..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: "[FEATURE]" -labels: '' -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/close-untitled-issues.yml b/.github/workflows/close-untitled-issues.yml index 0d7b740..3495bdf 100644 --- a/.github/workflows/close-untitled-issues.yml +++ b/.github/workflows/close-untitled-issues.yml @@ -19,7 +19,7 @@ jobs: script: | const title = context.payload.issue.title.trim(); const badTitles = [ - "[BUG]", + "[bug]", "bug report", "bug", "issue", @@ -64,4 +64,4 @@ jobs: state: "closed", state_reason: "not_planned" }); - } + } \ No newline at end of file diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml index 6a5e355..0ab074b 100644 --- a/chart/templates/deployment.yaml +++ b/chart/templates/deployment.yaml @@ -11,6 +11,9 @@ spec: app: {{ include "trek.name" . }} template: metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} labels: app: {{ include "trek.name" . }} spec: diff --git a/client/src/api/authUrl.ts b/client/src/api/authUrl.ts index ed92729..240d130 100644 --- a/client/src/api/authUrl.ts +++ b/client/src/api/authUrl.ts @@ -14,3 +14,45 @@ export async function getAuthUrl(url: string, purpose: string): Promise return url } } + +// ── Blob-based image fetching (Safari-safe, no ephemeral tokens needed) ──── + +const MAX_CONCURRENT = 6 +let active = 0 +const queue: Array<() => void> = [] + +function dequeue() { + while (active < MAX_CONCURRENT && queue.length > 0) { + active++ + queue.shift()!() + } +} + +export function clearImageQueue() { + queue.length = 0 +} + +export async function fetchImageAsBlob(url: string): Promise { + if (!url) return '' + return new Promise((resolve) => { + const run = async () => { + try { + const resp = await fetch(url, { credentials: 'include' }) + if (!resp.ok) { resolve(''); return } + const blob = await resp.blob() + resolve(URL.createObjectURL(blob)) + } catch { + resolve('') + } finally { + active-- + dequeue() + } + } + if (active < MAX_CONCURRENT) { + active++ + run() + } else { + queue.push(run) + } + }) +} diff --git a/client/src/components/Budget/BudgetPanel.tsx b/client/src/components/Budget/BudgetPanel.tsx index 0f447bb..a0cbd3d 100644 --- a/client/src/components/Budget/BudgetPanel.tsx +++ b/client/src/components/Budget/BudgetPanel.tsx @@ -489,7 +489,7 @@ export default function BudgetPanel({ tripId, tripMembers = [] }: BudgetPanelPro const d = currencyDecimals(currency) const fmtPrice = (v: number | null | undefined) => v != null ? v.toFixed(d) : '' - const fmtDate = (iso: string) => { if (!iso) return ''; const d = new Date(iso + 'T00:00:00'); return d.toLocaleDateString(locale, { day: '2-digit', month: '2-digit', year: 'numeric' }) } + const fmtDate = (iso: string) => { if (!iso) return ''; const d = new Date(iso + 'T00:00:00Z'); return d.toLocaleDateString(locale, { day: '2-digit', month: '2-digit', year: 'numeric', timeZone: 'UTC' }) } const header = ['Category', 'Name', 'Date', 'Total (' + currency + ')', 'Persons', 'Days', 'Per Person', 'Per Day', 'Per Person/Day', 'Note'] const rows = [header.join(sep)] diff --git a/client/src/components/Collab/WhatsNextWidget.tsx b/client/src/components/Collab/WhatsNextWidget.tsx index 6ad678d..c5fd11a 100644 --- a/client/src/components/Collab/WhatsNextWidget.tsx +++ b/client/src/components/Collab/WhatsNextWidget.tsx @@ -23,7 +23,7 @@ function formatDayLabel(date, t, locale) { if (d.toDateString() === now.toDateString()) return t('collab.whatsNext.today') || 'Today' if (d.toDateString() === tomorrow.toDateString()) return t('collab.whatsNext.tomorrow') || 'Tomorrow' - return d.toLocaleDateString(locale || undefined, { weekday: 'short', day: 'numeric', month: 'short' }) + return new Date(date + 'T00:00:00Z').toLocaleDateString(locale || undefined, { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' }) } interface TripMember { diff --git a/client/src/components/Memories/MemoriesPanel.tsx b/client/src/components/Memories/MemoriesPanel.tsx index 4614f6d..ad9be29 100644 --- a/client/src/components/Memories/MemoriesPanel.tsx +++ b/client/src/components/Memories/MemoriesPanel.tsx @@ -3,7 +3,7 @@ import { Camera, Plus, Share2, EyeOff, Eye, X, Check, Search, ArrowUpDown, MapPi import apiClient, { addonsApi } from '../../api/client' import { useAuthStore } from '../../store/authStore' import { useTranslation } from '../../i18n' -import { getAuthUrl } from '../../api/authUrl' +import { getAuthUrl, fetchImageAsBlob, clearImageQueue } from '../../api/authUrl' import { useToast } from '../shared/Toast' interface PhotoProvider { @@ -16,8 +16,13 @@ interface PhotoProvider { function ProviderImg({ baseUrl, provider, style, loading }: { baseUrl: string; provider: string; style?: React.CSSProperties; loading?: 'lazy' | 'eager' }) { const [src, setSrc] = useState('') useEffect(() => { - getAuthUrl(baseUrl, provider).then(setSrc).catch(() => {}) - }, [baseUrl, provider]) + let revoke = '' + fetchImageAsBlob(baseUrl).then(blobUrl => { + revoke = blobUrl + setSrc(blobUrl) + }) + return () => { if (revoke) URL.revokeObjectURL(revoke) } + }, [baseUrl]) return src ? : null } @@ -296,6 +301,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa shared: true, }) setShowPicker(false) + clearImageQueue() loadInitial() } catch { toast.error(t('memories.error.addPhotos')) } } @@ -500,7 +506,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
- @@ -769,9 +775,10 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
{ - setLightboxId(photo.asset_id); setLightboxUserId(photo.user_id); setLightboxInfo(null) + setLightboxId(photo.immich_asset_id); setLightboxUserId(photo.user_id); setLightboxInfo(null) + if (lightboxOriginalSrc) URL.revokeObjectURL(lightboxOriginalSrc) setLightboxOriginalSrc('') - getAuthUrl(`/api/integrations/${photo.provider}/assets/${photo.asset_id}/original?userId=${photo.user_id}`, photo.provider).then(setLightboxOriginalSrc).catch(() => {}) + fetchImageAsBlob(`/api/integrations/immich/assets/${photo.immich_asset_id}/original?userId=${photo.user_id}`).then(setLightboxOriginalSrc) setLightboxInfoLoading(true) apiClient.get(`/integrations/${photo.provider}/assets/${photo.asset_id}/info?userId=${photo.user_id}`) .then(r => setLightboxInfo(r.data)).catch(() => {}).finally(() => setLightboxInfoLoading(false)) @@ -879,12 +886,12 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa {/* Lightbox */} {lightboxId && lightboxUserId && ( -
{ setLightboxId(null); setLightboxUserId(null) }} +
{ if (lightboxOriginalSrc) URL.revokeObjectURL(lightboxOriginalSrc); setLightboxOriginalSrc(''); setLightboxId(null); setLightboxUserId(null) }} style={{ position: 'absolute', inset: 0, zIndex: 100, background: 'rgba(0,0,0,0.92)', display: 'flex', alignItems: 'center', justifyContent: 'center', }}> - diff --git a/client/src/components/Planner/ReservationModal.tsx b/client/src/components/Planner/ReservationModal.tsx index dbc3ab3..8a376d7 100644 --- a/client/src/components/Planner/ReservationModal.tsx +++ b/client/src/components/Planner/ReservationModal.tsx @@ -572,6 +572,6 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p function formatDate(dateStr, locale) { if (!dateStr) return '' - const d = new Date(dateStr + 'T00:00:00') - return d.toLocaleDateString(locale || undefined, { day: 'numeric', month: 'short' }) + const d = new Date(dateStr + 'T00:00:00Z') + return d.toLocaleDateString(locale || undefined, { day: 'numeric', month: 'short', timeZone: 'UTC' }) } diff --git a/client/src/components/Planner/ReservationsPanel.tsx b/client/src/components/Planner/ReservationsPanel.tsx index b6f9c55..4d37fab 100644 --- a/client/src/components/Planner/ReservationsPanel.tsx +++ b/client/src/components/Planner/ReservationsPanel.tsx @@ -84,8 +84,8 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo } const fmtDate = (str) => { - const d = new Date(str) - return d.toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short' }) + const dateOnly = str.includes('T') ? str.split('T')[0] : str + return new Date(dateOnly + 'T00:00:00Z').toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' }) } const fmtTime = (str) => { const d = new Date(str) diff --git a/client/src/components/Trips/TripFormModal.tsx b/client/src/components/Trips/TripFormModal.tsx index 4e834b9..aff4012 100644 --- a/client/src/components/Trips/TripFormModal.tsx +++ b/client/src/components/Trips/TripFormModal.tsx @@ -197,10 +197,10 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp if (!prev.end_date || prev.end_date < value) { next.end_date = value } else if (prev.start_date) { - const oldStart = new Date(prev.start_date + 'T00:00:00') - const oldEnd = new Date(prev.end_date + 'T00:00:00') + const oldStart = new Date(prev.start_date + 'T00:00:00Z') + const oldEnd = new Date(prev.end_date + 'T00:00:00Z') const duration = Math.round((oldEnd - oldStart) / 86400000) - const newEnd = new Date(value + 'T00:00:00') + const newEnd = new Date(value + 'T00:00:00Z') newEnd.setDate(newEnd.getDate() + duration) next.end_date = newEnd.toISOString().split('T')[0] } diff --git a/client/src/components/Vacay/holidays.ts b/client/src/components/Vacay/holidays.ts index fe5903b..61f92a9 100644 --- a/client/src/components/Vacay/holidays.ts +++ b/client/src/components/Vacay/holidays.ts @@ -104,18 +104,18 @@ export function getHolidays(year: number, bundesland: string = 'NW'): Record(null) const dropRef = useRef(null) - const parsed = value ? new Date(value + 'T00:00:00') : null - const [viewYear, setViewYear] = useState(parsed?.getFullYear() || new Date().getFullYear()) - const [viewMonth, setViewMonth] = useState(parsed?.getMonth() ?? new Date().getMonth()) + const parsed = value ? new Date(value + 'T00:00:00Z') : null + const [viewYear, setViewYear] = useState(parsed?.getUTCFullYear() || new Date().getFullYear()) + const [viewMonth, setViewMonth] = useState(parsed?.getUTCMonth() ?? new Date().getMonth()) useEffect(() => { const handler = (e: MouseEvent) => { @@ -36,7 +36,7 @@ export function CustomDatePicker({ value, onChange, placeholder, style = {}, com }, [open]) useEffect(() => { - if (open && parsed) { setViewYear(parsed.getFullYear()); setViewMonth(parsed.getMonth()) } + if (open && parsed) { setViewYear(parsed.getUTCFullYear()); setViewMonth(parsed.getUTCMonth()) } }, [open]) const prevMonth = () => { if (viewMonth === 0) { setViewMonth(11); setViewYear(y => y - 1) } else setViewMonth(m => m - 1) } @@ -47,7 +47,7 @@ export function CustomDatePicker({ value, onChange, placeholder, style = {}, com const startDay = (getWeekday(viewYear, viewMonth, 1) + 6) % 7 const weekdays = Array.from({ length: 7 }, (_, i) => new Date(2024, 0, i + 1).toLocaleDateString(locale, { weekday: 'narrow' })) - const displayValue = parsed ? parsed.toLocaleDateString(locale, compact ? { day: '2-digit', month: '2-digit', year: '2-digit' } : { day: 'numeric', month: 'short', year: 'numeric' }) : null + const displayValue = parsed ? parsed.toLocaleDateString(locale, compact ? { day: '2-digit', month: '2-digit', year: '2-digit', timeZone: 'UTC' } : { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' }) : null const selectDay = (day: number) => { const y = String(viewYear) @@ -57,7 +57,7 @@ export function CustomDatePicker({ value, onChange, placeholder, style = {}, com setOpen(false) } - const selectedDay = parsed && parsed.getFullYear() === viewYear && parsed.getMonth() === viewMonth ? parsed.getDate() : null + const selectedDay = parsed && parsed.getUTCFullYear() === viewYear && parsed.getUTCMonth() === viewMonth ? parsed.getUTCDate() : null const today = new Date() const isToday = (d: number) => today.getFullYear() === viewYear && today.getMonth() === viewMonth && today.getDate() === d diff --git a/client/src/i18n/translations/ar.ts b/client/src/i18n/translations/ar.ts index 4ba26be..a2523e7 100644 --- a/client/src/i18n/translations/ar.ts +++ b/client/src/i18n/translations/ar.ts @@ -598,7 +598,8 @@ const ar: Record = { 'vacay.subtitle': 'خطط وأدر أيام الإجازة', 'vacay.settings': 'الإعدادات', 'vacay.year': 'السنة', - 'vacay.addYear': 'إضافة سنة', + 'vacay.addYear': 'إضافة السنة التالية', + 'vacay.addPrevYear': 'إضافة السنة السابقة', 'vacay.removeYear': 'إزالة السنة', 'vacay.removeYearConfirm': 'إزالة {year}؟', 'vacay.removeYearHint': 'سيتم حذف كل إدخالات الإجازات والعطل الخاصة بهذه السنة نهائيًا.', diff --git a/client/src/i18n/translations/br.ts b/client/src/i18n/translations/br.ts index c51907d..4c2eb59 100644 --- a/client/src/i18n/translations/br.ts +++ b/client/src/i18n/translations/br.ts @@ -579,7 +579,8 @@ const br: Record = { 'vacay.subtitle': 'Planeje e gerencie dias de férias', 'vacay.settings': 'Configurações', 'vacay.year': 'Ano', - 'vacay.addYear': 'Adicionar ano', + 'vacay.addYear': 'Adicionar próximo ano', + 'vacay.addPrevYear': 'Adicionar ano anterior', 'vacay.removeYear': 'Remover ano', 'vacay.removeYearConfirm': 'Remover {year}?', 'vacay.removeYearHint': 'Todas as entradas de férias e feriados da empresa deste ano serão excluídas permanentemente.', diff --git a/client/src/i18n/translations/cs.ts b/client/src/i18n/translations/cs.ts index 1bf8e1d..669d496 100644 --- a/client/src/i18n/translations/cs.ts +++ b/client/src/i18n/translations/cs.ts @@ -597,7 +597,8 @@ const cs: Record = { 'vacay.subtitle': 'Plánování a správa dovolené', 'vacay.settings': 'Nastavení', 'vacay.year': 'Rok', - 'vacay.addYear': 'Přidat rok', + 'vacay.addYear': 'Přidat následující rok', + 'vacay.addPrevYear': 'Přidat předchozí rok', 'vacay.removeYear': 'Odebrat rok', 'vacay.removeYearConfirm': 'Odebrat rok {year}?', 'vacay.removeYearHint': 'Všechny záznamy o dovolené a firemní svátky pro tento rok budou trvale smazány.', diff --git a/client/src/i18n/translations/de.ts b/client/src/i18n/translations/de.ts index 7ebb8c0..a2980ba 100644 --- a/client/src/i18n/translations/de.ts +++ b/client/src/i18n/translations/de.ts @@ -595,7 +595,8 @@ const de: Record = { 'vacay.subtitle': 'Urlaubstage planen und verwalten', 'vacay.settings': 'Einstellungen', 'vacay.year': 'Jahr', - 'vacay.addYear': 'Jahr hinzufügen', + 'vacay.addYear': 'Nächstes Jahr hinzufügen', + 'vacay.addPrevYear': 'Vorheriges Jahr hinzufügen', 'vacay.removeYear': 'Jahr entfernen', 'vacay.removeYearConfirm': '{year} entfernen?', 'vacay.removeYearHint': 'Alle Urlaubseinträge und Betriebsferien für dieses Jahr werden unwiderruflich gelöscht.', diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts index 9f1c3f6..c2107e6 100644 --- a/client/src/i18n/translations/en.ts +++ b/client/src/i18n/translations/en.ts @@ -592,7 +592,8 @@ const en: Record = { 'vacay.subtitle': 'Plan and manage vacation days', 'vacay.settings': 'Settings', 'vacay.year': 'Year', - 'vacay.addYear': 'Add year', + 'vacay.addYear': 'Add next year', + 'vacay.addPrevYear': 'Add previous year', 'vacay.removeYear': 'Remove year', 'vacay.removeYearConfirm': 'Remove {year}?', 'vacay.removeYearHint': 'All vacation entries and company holidays for this year will be permanently deleted.', diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index bafee34..b1142fd 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -572,7 +572,8 @@ const es: Record = { 'vacay.subtitle': 'Planifica y gestiona días de vacaciones', 'vacay.settings': 'Ajustes', 'vacay.year': 'Año', - 'vacay.addYear': 'Añadir año', + 'vacay.addYear': 'Añadir año siguiente', + 'vacay.addPrevYear': 'Añadir año anterior', 'vacay.removeYear': 'Eliminar año', 'vacay.removeYearConfirm': '¿Eliminar {year}?', 'vacay.removeYearHint': 'Todas las vacaciones y festivos de empresa de este año se borrarán permanentemente.', diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts index 4727ff7..c37207d 100644 --- a/client/src/i18n/translations/fr.ts +++ b/client/src/i18n/translations/fr.ts @@ -594,7 +594,8 @@ const fr: Record = { 'vacay.subtitle': 'Planifiez et gérez vos jours de congés', 'vacay.settings': 'Paramètres', 'vacay.year': 'Année', - 'vacay.addYear': 'Ajouter une année', + 'vacay.addYear': 'Ajouter l\'année suivante', + 'vacay.addPrevYear': 'Ajouter l\'année précédente', 'vacay.removeYear': 'Supprimer l\'année', 'vacay.removeYearConfirm': 'Supprimer {year} ?', 'vacay.removeYearHint': 'Toutes les entrées de vacances et jours fériés d\'entreprise de cette année seront définitivement supprimés.', diff --git a/client/src/i18n/translations/hu.ts b/client/src/i18n/translations/hu.ts index 91211d8..05c72e1 100644 --- a/client/src/i18n/translations/hu.ts +++ b/client/src/i18n/translations/hu.ts @@ -595,7 +595,8 @@ const hu: Record = { 'vacay.subtitle': 'Szabadságnapok tervezése és kezelése', 'vacay.settings': 'Beállítások', 'vacay.year': 'Év', - 'vacay.addYear': 'Év hozzáadása', + 'vacay.addYear': 'Következő év hozzáadása', + 'vacay.addPrevYear': 'Előző év hozzáadása', 'vacay.removeYear': 'Év eltávolítása', 'vacay.removeYearConfirm': '{year} eltávolítása?', 'vacay.removeYearHint': 'Az adott év összes szabadság-bejegyzése és céges szabadnapja véglegesen törlődik.', diff --git a/client/src/i18n/translations/it.ts b/client/src/i18n/translations/it.ts index bebaf16..e1d76e2 100644 --- a/client/src/i18n/translations/it.ts +++ b/client/src/i18n/translations/it.ts @@ -595,7 +595,8 @@ const it: Record = { 'vacay.subtitle': 'Pianifica e gestisci i giorni di ferie', 'vacay.settings': 'Impostazioni', 'vacay.year': 'Anno', - 'vacay.addYear': 'Aggiungi anno', + 'vacay.addYear': 'Aggiungi anno successivo', + 'vacay.addPrevYear': 'Aggiungi anno precedente', 'vacay.removeYear': 'Rimuovi anno', 'vacay.removeYearConfirm': 'Rimuovere {year}?', 'vacay.removeYearHint': 'Tutte le voci delle ferie e le ferie aziendali di questo anno verranno eliminate in modo permanente.', diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts index 814be9b..c40f87a 100644 --- a/client/src/i18n/translations/nl.ts +++ b/client/src/i18n/translations/nl.ts @@ -594,7 +594,8 @@ const nl: Record = { 'vacay.subtitle': 'Plan en beheer vakantiedagen', 'vacay.settings': 'Instellingen', 'vacay.year': 'Jaar', - 'vacay.addYear': 'Jaar toevoegen', + 'vacay.addYear': 'Volgend jaar toevoegen', + 'vacay.addPrevYear': 'Vorig jaar toevoegen', 'vacay.removeYear': 'Jaar verwijderen', 'vacay.removeYearConfirm': '{year} verwijderen?', 'vacay.removeYearHint': 'Alle vakantie-invoeren en bedrijfsvakanties voor dit jaar worden permanent verwijderd.', diff --git a/client/src/i18n/translations/pl.ts b/client/src/i18n/translations/pl.ts index 36d9e9f..ecd3b8a 100644 --- a/client/src/i18n/translations/pl.ts +++ b/client/src/i18n/translations/pl.ts @@ -558,7 +558,8 @@ const pl: Record = { 'vacay.subtitle': 'Planuj i zarządzaj dniami urlopu', 'vacay.settings': 'Ustawienia', 'vacay.year': 'Rok', - 'vacay.addYear': 'Dodaj rok', + 'vacay.addYear': 'Dodaj następny rok', + 'vacay.addPrevYear': 'Dodaj poprzedni rok', 'vacay.removeYear': 'Usuń rok', 'vacay.removeYearConfirm': 'Usunąć {year}?', 'vacay.removeYearHint': 'Wszystkie wpisy dotyczące urlopów oraz dni wolnych w tym roku zostaną trwale usunięte.', diff --git a/client/src/i18n/translations/ru.ts b/client/src/i18n/translations/ru.ts index 997e25b..518c5e9 100644 --- a/client/src/i18n/translations/ru.ts +++ b/client/src/i18n/translations/ru.ts @@ -594,7 +594,8 @@ const ru: Record = { 'vacay.subtitle': 'Планируйте и управляйте днями отпуска', 'vacay.settings': 'Настройки', 'vacay.year': 'Год', - 'vacay.addYear': 'Добавить год', + 'vacay.addYear': 'Добавить следующий год', + 'vacay.addPrevYear': 'Добавить предыдущий год', 'vacay.removeYear': 'Удалить год', 'vacay.removeYearConfirm': 'Удалить {year}?', 'vacay.removeYearHint': 'Все записи об отпуске и корпоративные выходные за этот год будут безвозвратно удалены.', diff --git a/client/src/i18n/translations/zh.ts b/client/src/i18n/translations/zh.ts index 5fd14e4..5a90e5b 100644 --- a/client/src/i18n/translations/zh.ts +++ b/client/src/i18n/translations/zh.ts @@ -594,7 +594,8 @@ const zh: Record = { 'vacay.subtitle': '规划和管理假期', 'vacay.settings': '设置', 'vacay.year': '年份', - 'vacay.addYear': '添加年份', + 'vacay.addYear': '添加下一年', + 'vacay.addPrevYear': '添加上一年', 'vacay.removeYear': '移除年份', 'vacay.removeYearConfirm': '移除 {year}?', 'vacay.removeYearHint': '该年度所有假期记录和公司假日将被永久删除。', diff --git a/client/src/pages/DashboardPage.tsx b/client/src/pages/DashboardPage.tsx index 16f36d0..a386379 100644 --- a/client/src/pages/DashboardPage.tsx +++ b/client/src/pages/DashboardPage.tsx @@ -59,12 +59,12 @@ function getTripStatus(trip: DashboardTrip): string | null { function formatDate(dateStr: string | null | undefined, locale: string = 'en-US'): string | null { if (!dateStr) return null - return new Date(dateStr + 'T00:00:00').toLocaleDateString(locale, { day: 'numeric', month: 'short', year: 'numeric' }) + return new Date(dateStr + 'T00:00:00Z').toLocaleDateString(locale, { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' }) } function formatDateShort(dateStr: string | null | undefined, locale: string = 'en-US'): string | null { if (!dateStr) return null - return new Date(dateStr + 'T00:00:00').toLocaleDateString(locale, { day: 'numeric', month: 'short' }) + return new Date(dateStr + 'T00:00:00Z').toLocaleDateString(locale, { day: 'numeric', month: 'short', timeZone: 'UTC' }) } function sortTrips(trips: DashboardTrip[]): DashboardTrip[] { diff --git a/client/src/pages/SharedTripPage.tsx b/client/src/pages/SharedTripPage.tsx index d8fb54a..09fa866 100644 --- a/client/src/pages/SharedTripPage.tsx +++ b/client/src/pages/SharedTripPage.tsx @@ -106,7 +106,7 @@ export default function SharedTripPage() { {(trip.start_date || trip.end_date) && (
- {[trip.start_date, trip.end_date].filter(Boolean).map((d: string) => new Date(d + 'T00:00:00').toLocaleDateString(locale, { day: 'numeric', month: 'short', year: 'numeric' })).join(' — ')} + {[trip.start_date, trip.end_date].filter(Boolean).map((d: string) => new Date(d + 'T00:00:00Z').toLocaleDateString(locale, { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' })).join(' — ')} {days?.length > 0 && ·} {days?.length > 0 && {days.length} {t('shared.days')}} @@ -199,7 +199,7 @@ export default function SharedTripPage() {
{di + 1}
{day.title || `Day ${day.day_number}`}
- {day.date &&
{new Date(day.date + 'T00:00:00').toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short' })}
} + {day.date &&
{new Date(day.date + 'T00:00:00Z').toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' })}
}
{dayAccs.map((acc: any) => ( @@ -274,7 +274,7 @@ export default function SharedTripPage() { const meta = typeof r.metadata === 'string' ? JSON.parse(r.metadata || '{}') : (r.metadata || {}) const TIcon = TRANSPORT_ICONS[r.type] || Ticket const time = r.reservation_time?.includes('T') ? r.reservation_time.split('T')[1]?.substring(0, 5) : '' - const date = r.reservation_time ? new Date(r.reservation_time.includes('T') ? r.reservation_time : r.reservation_time + 'T00:00:00').toLocaleDateString(locale, { day: 'numeric', month: 'short' }) : '' + const date = r.reservation_time ? new Date((r.reservation_time.includes('T') ? r.reservation_time.split('T')[0] : r.reservation_time) + 'T00:00:00Z').toLocaleDateString(locale, { day: 'numeric', month: 'short', timeZone: 'UTC' }) : '' return (
diff --git a/client/src/pages/VacayPage.tsx b/client/src/pages/VacayPage.tsx index 68efd34..b3a524e 100644 --- a/client/src/pages/VacayPage.tsx +++ b/client/src/pages/VacayPage.tsx @@ -41,11 +41,16 @@ export default function VacayPage(): React.ReactElement { if (selectedYear) { loadEntries(selectedYear); loadStats(selectedYear); loadHolidays(selectedYear) } }, [selectedYear]) - const handleAddYear = () => { + const handleAddNextYear = () => { const nextYear = years.length > 0 ? Math.max(...years) + 1 : new Date().getFullYear() addYear(nextYear) } + const handleAddPrevYear = () => { + const prevYear = years.length > 0 ? Math.min(...years) - 1 : new Date().getFullYear() + addYear(prevYear) + } + if (loading) { return (
@@ -62,20 +67,27 @@ export default function VacayPage(): React.ReactElement { <> {/* Year Selector */}
-
+
{t('vacay.year')} -
- +
+ + +
{selectedYear} - +
+ + +
{years.map(y => ( diff --git a/client/src/store/vacayStore.ts b/client/src/store/vacayStore.ts index f1d7ef7..8e87664 100644 --- a/client/src/store/vacayStore.ts +++ b/client/src/store/vacayStore.ts @@ -222,7 +222,14 @@ export const useVacayStore = create((set, get) => ({ removeYear: async (year: number) => { const data = await api.removeYear(year) - set({ years: data.years }) + const updates: Partial = { years: data.years } + if (get().selectedYear === year) { + updates.selectedYear = data.years.length > 0 + ? data.years[data.years.length - 1] + : new Date().getFullYear() + } + set(updates) + await get().loadStats() }, loadEntries: async (year?: number) => { @@ -240,6 +247,7 @@ export const useVacayStore = create((set, get) => ({ toggleCompanyHoliday: async (date: string) => { await api.toggleCompanyHoliday(date) await get().loadEntries() + await get().loadStats() }, loadStats: async (year?: number) => { diff --git a/client/src/utils/formatters.ts b/client/src/utils/formatters.ts index 7e7ebc0..980f85a 100644 --- a/client/src/utils/formatters.ts +++ b/client/src/utils/formatters.ts @@ -10,9 +10,9 @@ export function formatDate(dateStr: string | null | undefined, locale: string, t if (!dateStr) return null const opts: Intl.DateTimeFormatOptions = { weekday: 'short', day: 'numeric', month: 'short', + timeZone: timeZone || 'UTC', } - if (timeZone) opts.timeZone = timeZone - return new Date(dateStr + 'T00:00:00').toLocaleDateString(locale, opts) + return new Date(dateStr + 'T00:00:00Z').toLocaleDateString(locale, opts) } export function formatTime(timeStr: string | null | undefined, locale: string, timeFormat: string): string { diff --git a/server/src/services/vacayService.ts b/server/src/services/vacayService.ts index ba5aa43..c1607dc 100644 --- a/server/src/services/vacayService.ts +++ b/server/src/services/vacayService.ts @@ -496,6 +496,30 @@ export function deleteYear(planId: number, year: number, socketId: string | unde db.prepare('DELETE FROM vacay_years WHERE plan_id = ? AND year = ?').run(planId, year); db.prepare("DELETE FROM vacay_entries WHERE plan_id = ? AND date LIKE ?").run(planId, `${year}-%`); db.prepare("DELETE FROM vacay_company_holidays WHERE plan_id = ? AND date LIKE ?").run(planId, `${year}-%`); + db.prepare('DELETE FROM vacay_user_years WHERE plan_id = ? AND year = ?').run(planId, year); + + // Recalculate carry-over for year+1 if it exists, since its previous year has changed + const nextYearExists = db.prepare('SELECT id FROM vacay_years WHERE plan_id = ? AND year = ?').get(planId, year + 1); + if (nextYearExists) { + const plan = db.prepare('SELECT * FROM vacay_plans WHERE id = ?').get(planId) as VacayPlan | undefined; + const carryOverEnabled = plan ? !!plan.carry_over_enabled : true; + const users = getPlanUsers(planId); + const prevYear = db.prepare('SELECT year FROM vacay_years WHERE plan_id = ? AND year < ? ORDER BY year DESC LIMIT 1').get(planId, year + 1) as { year: number } | undefined; + + for (const u of users) { + let carry = 0; + if (carryOverEnabled && prevYear) { + const prevConfig = db.prepare('SELECT * FROM vacay_user_years WHERE user_id = ? AND plan_id = ? AND year = ?').get(u.id, planId, prevYear.year) as VacayUserYear | undefined; + if (prevConfig) { + const used = (db.prepare("SELECT COUNT(*) as count FROM vacay_entries WHERE user_id = ? AND plan_id = ? AND date LIKE ?").get(u.id, planId, `${prevYear.year}-%`) as { count: number }).count; + const total = prevConfig.vacation_days + prevConfig.carried_over; + carry = Math.max(0, total - used); + } + } + db.prepare('UPDATE vacay_user_years SET carried_over = ? WHERE user_id = ? AND plan_id = ? AND year = ?').run(carry, u.id, planId, year + 1); + } + } + notifyPlanUsers(planId, socketId, 'vacay:settings'); return listYears(planId); } diff --git a/unraid-template.xml b/unraid-template.xml index 8fe29a1..e239673 100644 --- a/unraid-template.xml +++ b/unraid-template.xml @@ -21,10 +21,41 @@ Support TREK development https://ko-fi.com/mauriceboe + + 3000 /mnt/user/appdata/trek/data /mnt/user/appdata/trek/uploads - production - - 3000 + + + + 3000 + production + UTC + info + + + false + true + 1 + false + + + admin@trek.local + + + + + + + SSO + false + + + openid email profile groups + + + + false + 60