diff --git a/.github/ISSUE_TEMPLATE/translation_fix.yml b/.github/ISSUE_TEMPLATE/translation_fix.yml new file mode 100644 index 00000000..95a389a6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/translation_fix.yml @@ -0,0 +1,63 @@ +name: Translation improvement +description: Suggest a correction or better wording for interface text in a specific language (no Git required). +title: "[i18n] " +labels: [] +body: + - type: markdown + attributes: + value: | + Thank you for helping improve translations. Before submitting, skim **Rules for translators** in `docs/CONTRIBUTING_TRANSLATIONS.md` in this repository (placeholders, context, what not to change). + + - type: dropdown + id: language + attributes: + label: Language + description: Locale code for the translation you are improving. + options: + - nl (Nederlands) + - de (Deutsch) + - fr (Français) + - it (Italiano) + - fi (Suomi) + - es (Español) + - no (Norsk) + - ar (العربية) + - he (עברית) + - en (English) + validations: + required: true + + - type: input + id: location + attributes: + label: Where did you see this? + description: Page name, menu path, or URL path (e.g. Dashboard, Settings, /login). + validations: + required: true + + - type: textarea + id: current_text + attributes: + label: Current text (as shown in the app) + description: Copy the exact wording from the UI in the language you selected. + validations: + required: true + + - type: textarea + id: suggested_text + attributes: + label: Suggested text + description: Your improved translation. Keep any %(name)s-style placeholders identical to the current text if present. + validations: + required: true + + - type: textarea + id: notes + attributes: + label: Notes (optional) + description: Why this reads better, grammar fix, formal vs informal tone, screenshot description, etc. + + - type: markdown + attributes: + value: | + **Optional:** Attach a screenshot in a follow-up comment if the form does not allow images. diff --git a/.github/workflows/crowdin-sync.yml b/.github/workflows/crowdin-sync.yml new file mode 100644 index 00000000..96e0ba20 --- /dev/null +++ b/.github/workflows/crowdin-sync.yml @@ -0,0 +1,34 @@ +# Manual Crowdin sync: uploads English source .po, downloads translations, opens a PR. +# Prerequisites: repo secrets CROWDIN_PROJECT_ID and CROWDIN_PERSONAL_TOKEN (see docs/CONTRIBUTING_TRANSLATIONS.md). +name: Crowdin sync + +on: + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + crowdin: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Crowdin push and pull + uses: crowdin/github-action@v2 + with: + upload_sources: true + # Set to true once to seed Crowdin with existing translations/…/messages.po, then set back to false. + upload_translations: false + download_translations: true + localization_branch_name: i18n/crowdin + create_pull_request: true + pull_request_title: "chore(i18n): Crowdin translations" + pull_request_body: "Automated sync from Crowdin. Review placeholders and `no` vs `nb` paths before merge." + commit_message: "chore(i18n): sync Crowdin translations" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index e3aff8da..ba3e3b47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,16 +8,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Fixed -- **Quote create returned HTTP 500 after save (#583)** — The quote was persisted, but the redirect to the quote detail page crashed because the view expected `requires_approval` and `can_be_sent`, and templates could set `approval_level`, while the `Quote` model had no matching fields. Added `requires_approval` and `approval_level` columns, a `can_be_sent` property aligned with send rules, wired the create-form checkbox, fixed the approval banner branch to use `approval_status == 'not_required'`, migration `145_add_quotes_requires_approval`, and regression coverage in `tests/test_routes/test_quotes_web.py`. +- **Quote create returned HTTP 500 after save (#583)** — The quote was saved, but the redirect to the quote detail page crashed when **Valid until** was set: the template compared `valid_until` to `now()`, and `now` was never defined in the Jinja context. The expired badge now uses `Quote.is_expired` (same rule, app timezone). Regression coverage in `tests/test_routes/test_quotes_web.py` posts `valid_until` so the view path is exercised. +- **Desktop app navigation guard** — `will-navigate` no longer mis-classifies `file:` loads (opaque `"null"` origin) as external navigation. Allowed in-app protocols include `file:`, `about:`, and `devtools:`; `http:` / `https:` are still blocked from the embedded window. +- **Desktop offline UI (bundle)** — Shared helpers load before dependent modules; timesheet period and time-off request lists expose **Delete** where allowed (with `currentUserProfile.id` for ownership); approve/reject controls read approval state from `state.currentUserProfile`; API client includes `deleteTimesheetPeriod` and `deleteTimeOffRequest`. ### Added -- **Quote line item reorder (Issue #584)** — Non-null `quote_items.position` (migration `146_add_quote_item_position`); `Quote.items` is ordered by `position`, then `id`. Create, edit, duplicate, bulk duplicate, API item payloads, and quote-template apply assign positions from the submitted row order. **Create quote** and **edit quote** line tables include **Move up** / **Move down** per row so items can be reordered without deleting and re-entering lines; PDFs and detail views follow the saved order. +- **Quote line item reorder (Issue #584)** — Non-null `quote_items.position` (migration `146_add_quote_item_position`); `Quote.items` is ordered by `position`, then `id`. Create, edit, duplicate, bulk duplicate, API item payloads, and quote-template apply assign positions from the submitted row order. **Create quote** and **edit quote** forms include per-row **Move up** / **Move down** controls on **Quote line items**, **Costs**, and **Extra goods** so rows can be reordered without deleting and re-entering data; PDFs and detail views follow the saved order. New translatable UI strings: **Order**, **Move up**, **Move down** (run `pybabel extract` / `update` per [docs/CONTRIBUTING_TRANSLATIONS.md](docs/CONTRIBUTING_TRANSLATIONS.md)). - **Offline queue replay** — Queued requests now store method, headers, and body in a replay-safe form (serializable for localStorage). POST/PUT requests replayed when back online send the same body and method. Legacy queue items (with `options` only) are still replayed via fallback. - **Inventory API scopes** — New scopes `read:inventory` and `write:inventory` for inventory-only API access. Existing `read:projects` and `write:projects` still grant the same inventory access for backward compatibility. - **Client portal reports: date range and CSV export** — Reports support optional `days` query param (1–365, default 30). Add `?format=csv` to download a CSV of the same report (summary, hours by project, time by date). Export uses the same access control as the reports page. - **Jira webhook verification** — When a webhook secret is configured in the Jira integration (Connection Settings → Webhook Secret), incoming webhooks are verified using HMAC-SHA256 of the request body. Supported headers: `X-Hub-Signature-256`, `X-Atlassian-Webhook-Signature`, `X-Hub-Signature`. Requests with missing or invalid signature are rejected. If no secret is set, behavior is unchanged (all webhooks accepted). +- **Crowdin integration (maintainers)** — Root [`crowdin.yml`](crowdin.yml) maps `translations/en/LC_MESSAGES/messages.po` to per-locale `messages.po` paths (with `nb` → `no` for Norwegian). Manual [`.github/workflows/crowdin-sync.yml`](.github/workflows/crowdin-sync.yml) uploads sources and downloads translations when `CROWDIN_PROJECT_ID` and `CROWDIN_PERSONAL_TOKEN` are set. [docs/CONTRIBUTING_TRANSLATIONS.md](docs/CONTRIBUTING_TRANSLATIONS.md) includes a Crowdin setup section; [docs/TRANSLATION_SYSTEM.md](docs/TRANSLATION_SYSTEM.md) and contributor docs cross-link it. ### Changed +- **Documentation (translations)** — Added [docs/CONTRIBUTING_TRANSLATIONS.md](docs/CONTRIBUTING_TRANSLATIONS.md) for contributors without Git (issue template, optional spreadsheet or hosted platform, maintainer workflow). Root [CONTRIBUTING.md](CONTRIBUTING.md) links to it; [docs/TRANSLATION_SYSTEM.md](docs/TRANSLATION_SYSTEM.md) defers the enabled locale list to `app/config.py` (`LANGUAGES`) and points translators at the new guide. - **Factur-X / PDF/A-3 invoice PDFs (export and email)** — Download and email attachments use the same embed-and-normalize path. Embedded CII uses Associated File relationship **Data** and MIME **text/xml**. PDF/A-3 normalization embeds sRGB via `app/resources/icc/` (override with `INVOICE_SRGB_ICC_PATH`). Added `app/utils/invoice_pdf_postprocess.py` and tests; [PEPPOL e-Invoicing](docs/admin/configuration/PEPPOL_EINVOICING.md) updated (veraPDF note, pytest command). - **Documentation sync** — CODEBASE_AUDIT.md: marked gaps 2.3–2.7 and 2.9 as fixed; added “Implemented 2026-03-16” summary. CLIENT_FEATURES_IMPLEMENTATION_STATUS: report date range and CSV export noted as implemented. INCOMPLETE_IMPLEMENTATIONS_ANALYSIS: added “Verified 2026-03-16” for webhook verification, issues permissions, search API, offline queue. - **Activity feed API date params** — `/api/activity` now returns 400 with a clear message when `start_date` or `end_date` are invalid (e.g. not ISO 8601). Invalid dates on the web route `/activity` are logged and the filter is skipped (no 500). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2fe68d3d..e70ec9f8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,6 +5,7 @@ Thank you for your interest in contributing to TimeTracker. This page gives you ## How to Contribute - **Report bugs** — Use the [GitHub issue tracker](https://github.com/drytrix/TimeTracker/issues). Include steps to reproduce, expected vs actual behavior, and your environment (OS, deployment method, version). +- **Improve translations (no Git)** — Use the **Translation improvement** issue template, or read [docs/CONTRIBUTING_TRANSLATIONS.md](docs/CONTRIBUTING_TRANSLATIONS.md) for spreadsheet, maintainer workflow, and optional Crowdin setup ([`crowdin.yml`](crowdin.yml), **Actions → Crowdin sync**). - **Suggest features** — Open a [feature request](https://github.com/drytrix/TimeTracker/issues/new?template=feature_request.md) with a clear description and use case. - **Submit code** — Fork the repo, create a branch, make your changes, add tests, and open a pull request. Follow the [full contributing guidelines](docs/development/CONTRIBUTING.md) for setup, coding standards, and PR process. diff --git a/app/templates/quotes/_edit_quote_form_scripts.html b/app/templates/quotes/_edit_quote_form_scripts.html index a0cfd248..53e64d8d 100644 --- a/app/templates/quotes/_edit_quote_form_scripts.html +++ b/app/templates/quotes/_edit_quote_form_scripts.html @@ -10,6 +10,54 @@ document.addEventListener('DOMContentLoaded', function() { const stockItems = {{ stock_items_json | safe if stock_items_json else '[]' }}; const warehouses = {{ warehouses_json | safe if warehouses_json else '[]' }}; + function reorderButtonsHtml() { + return ( + '
' + + '' + + '
' + ); + } + + function refreshReorderStates(parent, rowSelector) { + if (!parent) return; + const rows = parent.querySelectorAll(rowSelector); + rows.forEach(function(r, i) { + const up = r.querySelector('.quote-line-move-up'); + const down = r.querySelector('.quote-line-move-down'); + if (up) up.disabled = i === 0; + if (down) down.disabled = i === rows.length - 1; + }); + } + + function moveRowInSection(row, delta) { + const parent = row.parentElement; + if (!parent) return; + var sel = null; + if (row.classList.contains('quote-item-row')) sel = '.quote-item-row'; + else if (row.classList.contains('quote-expense-row')) sel = '.quote-expense-row'; + else if (row.classList.contains('quote-good-row')) sel = '.quote-good-row'; + else return; + const rows = Array.from(parent.querySelectorAll(sel)); + const idx = rows.indexOf(row); + if (idx < 0) return; + const newIdx = idx + delta; + if (newIdx < 0 || newIdx >= rows.length) return; + const ref = rows[newIdx]; + if (delta < 0) parent.insertBefore(row, ref); + else parent.insertBefore(row, ref.nextSibling); + refreshReorderStates(parent, sel); + calculateTotals(); + } + + function wireReorderButtons(row) { + row.querySelector('.quote-line-move-up')?.addEventListener('click', function() { + moveRowInSection(row, -1); + }); + row.querySelector('.quote-line-move-down')?.addEventListener('click', function() { + moveRowInSection(row, 1); + }); + } + function stockOptionsHtml(selectedId) { let h = ''; if (stockItems && Array.isArray(stockItems)) { @@ -40,7 +88,7 @@ document.addEventListener('DOMContentLoaded', function() { if (!stockCols || !descWrap) return; if (stockMode) { stockCols.classList.remove('hidden'); - descWrap.classList.remove('md:col-span-6'); + descWrap.classList.remove('md:col-span-5'); descWrap.classList.add('md:col-span-2'); } else { stockCols.classList.add('hidden'); @@ -53,7 +101,7 @@ document.addEventListener('DOMContentLoaded', function() { if (ss) ss.value = ''; if (ws) ws.value = ''; descWrap.classList.remove('md:col-span-2'); - descWrap.classList.add('md:col-span-6'); + descWrap.classList.add('md:col-span-5'); } } @@ -94,12 +142,15 @@ document.addEventListener('DOMContentLoaded', function() { } row.querySelector('.remove-item')?.addEventListener('click', function() { row.remove(); + if (itemsContainer) refreshReorderStates(itemsContainer, '.quote-item-row'); calculateTotals(); }); + wireReorderButtons(row); row.querySelectorAll('[data-calc-trigger]').forEach(function(el) { el.addEventListener('input', calculateTotals); }); applyItemRowLayout(row); + if (itemsContainer) refreshReorderStates(itemsContainer, '.quote-item-row'); } function addItemRow() { @@ -109,14 +160,15 @@ document.addEventListener('DOMContentLoaded', function() { '' + '' + '' + + reorderButtonsHtml() + '
' + '
' + - '