Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
852c127c00 | ||
|
|
71a0016a4a | ||
|
|
88c9b7c2b8 | ||
|
|
4a78d31a7f | ||
|
|
e845400fca | ||
|
|
fce27633a9 | ||
|
|
4d47897e27 | ||
|
|
aef4ab92ec | ||
|
|
5f36c34d48 | ||
|
|
b94f4879e3 | ||
|
|
dab5ad8aa5 | ||
|
|
8b77a0a905 | ||
|
|
4b1c42b17f | ||
|
|
a55356d417 | ||
|
|
c983747418 | ||
|
|
3db257faa9 | ||
|
|
d3d7f7e199 | ||
|
|
084ec9289b | ||
|
|
4a9c283789 | ||
|
|
2f76fe0bc1 | ||
|
|
9a11a8f001 | ||
|
|
f0816b126b | ||
|
|
f31797fb5b | ||
|
|
9c110abf46 | ||
|
|
a0f351b0aa | ||
|
|
916cf34efd | ||
|
|
003807a074 | ||
|
|
ca911bf96c | ||
|
|
9c10ff9f22 | ||
|
|
38a58b55c0 | ||
|
|
4d8fd62961 | ||
|
|
ef404e4362 | ||
|
|
fb022802ae | ||
|
|
795d4d5af9 | ||
|
|
74113035ae | ||
|
|
42f9561337 | ||
|
|
f79967d433 | ||
|
|
d410e7433e | ||
|
|
0711bafac0 | ||
|
|
5273f88272 | ||
|
|
182d4ca4b3 | ||
|
|
4773b5be22 | ||
|
|
6074d6163b | ||
|
|
652e3f91fc | ||
|
|
5233db2002 | ||
|
|
dac45baaa8 | ||
|
|
f389c4b3cf | ||
|
|
fdc7037e60 | ||
|
|
b02f6afe93 | ||
|
|
1b2ab7b4eb | ||
|
|
29975ecad9 | ||
|
|
e846f15126 | ||
|
|
86bebf9776 | ||
|
|
7b7c6bde68 | ||
|
|
935fb235a6 | ||
|
|
d2d1cdbb92 | ||
|
|
5f08b0e086 | ||
|
|
637499a8ec | ||
|
|
f413bae348 | ||
|
|
2b1de583fd | ||
|
|
5500cdb118 | ||
|
|
2254a13ad0 | ||
|
|
1f5885de84 | ||
|
|
705f58e3d1 | ||
|
|
0330453ad6 | ||
|
|
66b98e2765 | ||
|
|
55648c9a30 |
@@ -1,43 +0,0 @@
|
|||||||
---
|
|
||||||
name: compare-php
|
|
||||||
description: Compare a feature between PHP boha-app and TS boha-app-ts to verify migration parity
|
|
||||||
---
|
|
||||||
|
|
||||||
# Compare PHP vs TS Implementation
|
|
||||||
|
|
||||||
Compare a specific feature, component, or endpoint between the original PHP project (D:\cortex\boha-app) and the TypeScript migration (D:\cortex\boha-app-ts).
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
The user will specify what to compare. Examples:
|
|
||||||
- `/compare-php attendance print`
|
|
||||||
- `/compare-php invoice PDF generation`
|
|
||||||
- `/compare-php offer numbering logic`
|
|
||||||
|
|
||||||
## Process
|
|
||||||
|
|
||||||
1. Search the PHP codebase (D:\cortex\boha-app) for the relevant implementation
|
|
||||||
2. Search the TS codebase (D:\cortex\boha-app-ts) for the same feature
|
|
||||||
3. Compare:
|
|
||||||
- API endpoints and request/response shape
|
|
||||||
- Business logic and calculations
|
|
||||||
- Database queries
|
|
||||||
- Frontend behavior
|
|
||||||
4. Report differences found — what's missing, what's different, what's extra
|
|
||||||
|
|
||||||
## Key directories
|
|
||||||
|
|
||||||
**PHP project:**
|
|
||||||
- API handlers: `D:\cortex\boha-app\api\admin\handlers\`
|
|
||||||
- API routes: `D:\cortex\boha-app\api\admin\`
|
|
||||||
- Frontend pages: `D:\cortex\boha-app\src\admin\pages\`
|
|
||||||
- Frontend components: `D:\cortex\boha-app\src\admin\components\`
|
|
||||||
- Frontend hooks: `D:\cortex\boha-app\src\admin\hooks\`
|
|
||||||
- Includes/utils: `D:\cortex\boha-app\api\includes\`
|
|
||||||
|
|
||||||
**TS project:**
|
|
||||||
- API routes: `D:\cortex\boha-app-ts\src\routes\admin\`
|
|
||||||
- Services: `D:\cortex\boha-app-ts\src\services\`
|
|
||||||
- Frontend pages: `D:\cortex\boha-app-ts\src\admin\pages\`
|
|
||||||
- Frontend components: `D:\cortex\boha-app-ts\src\admin\components\`
|
|
||||||
- Frontend hooks: `D:\cortex\boha-app-ts\src\admin\hooks\`
|
|
||||||
51
CLAUDE.md
51
CLAUDE.md
@@ -87,6 +87,8 @@ npx prisma migrate resolve --applied <migration> # Mark migration as applied
|
|||||||
|
|
||||||
**Do not start the dev server.** The user manages it separately.
|
**Do not start the dev server.** The user manages it separately.
|
||||||
|
|
||||||
|
**Before running `prisma migrate dev` or `prisma db push`, ask the user to stop their dev server and wait for confirmation.** Migrations can conflict with an active database connection from the running server.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
@@ -287,6 +289,23 @@ When entity A embeds/references entity B, A's mutation handlers invalidate B's d
|
|||||||
**Golden rule: NEVER use `prisma db push`.** It bypasses migration history and causes drift
|
**Golden rule: NEVER use `prisma db push`.** It bypasses migration history and causes drift
|
||||||
between the schema and migration tracking. Always use `prisma migrate dev` for every schema change.
|
between the schema and migration tracking. Always use `prisma migrate dev` for every schema change.
|
||||||
|
|
||||||
|
**Every database change or manipulation must be a tracked Prisma migration.** This includes:
|
||||||
|
|
||||||
|
- Schema changes (tables, columns, indexes, constraints) — via `prisma migrate dev`
|
||||||
|
- Permission/role/seed data changes — via a migration that does the INSERTs/DELETEs explicitly
|
||||||
|
- Lookup data, default settings, or any other row-level baseline that production needs
|
||||||
|
- Backfills, data fixes, and one-shot corrections that should be in sync across environments
|
||||||
|
|
||||||
|
**What is NOT acceptable:**
|
||||||
|
|
||||||
|
- Running raw SQL against production (`mysql -e "..."`, `npx prisma db execute`, `pm2 exec`)
|
||||||
|
- `npx prisma db seed` as the only way to populate data (seed is dev-only convenience; prod must run the same SQL via a migration)
|
||||||
|
- "I'll fix it in the seed file" or "I'll add a row manually" without a migration to back it
|
||||||
|
|
||||||
|
The reason: every change to the production database must be reviewable, reversible, and reproducible from a fresh checkout. External SQL commands and seed-only changes cause drift — prod and dev diverge, rollback gets harder with every manual change, and the next deploy surprises us.
|
||||||
|
|
||||||
|
If you need to insert/update/seed data on production, write a migration. The migration's `migration.sql` is a normal SQL file — `INSERT INTO ...`, `UPDATE ...`, `DELETE ...` are all valid. Prisma will run it via `prisma migrate deploy` like any other migration.
|
||||||
|
|
||||||
### Making schema changes
|
### Making schema changes
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -367,6 +386,8 @@ npx prisma migrate resolve --applied <migration_name>
|
|||||||
|
|
||||||
10. **Czech locale hardcoded** — Error messages, month names, and some business logic strings are Czech. This is intentional.
|
10. **Czech locale hardcoded** — Error messages, month names, and some business logic strings are Czech. This is intentional.
|
||||||
|
|
||||||
|
11. **Seed file is dev-only** — `prisma/seed.ts` is for local dev convenience. It is **not** the production data source. Any data the production database must have (permissions, default roles, baseline rows) belongs in a migration's `migration.sql`, not in the seed file. `npx prisma db seed` must never be run against production.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Release Process
|
## Release Process
|
||||||
@@ -385,4 +406,34 @@ npx prisma migrate resolve --applied <migration_name>
|
|||||||
- Apply Prisma migrations: `npx prisma migrate deploy`
|
- Apply Prisma migrations: `npx prisma migrate deploy`
|
||||||
- Restart: `pm2 restart app-ts --update-env`
|
- Restart: `pm2 restart app-ts --update-env`
|
||||||
|
|
||||||
|
### Hotfixing a migration that was added after the tarball was built
|
||||||
|
|
||||||
|
If you write a new `prisma/migrations/<timestamp>_*` folder **after** the
|
||||||
|
release tarball has already been built and shipped, that migration is
|
||||||
|
NOT in the tarball and `prisma migrate deploy` on prod will report
|
||||||
|
"No pending migrations". The release tarball re-build re-runs the whole
|
||||||
|
release, but for a one-off hotfix you can ship just the new migration
|
||||||
|
folder:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Local
|
||||||
|
cd prisma/migrations
|
||||||
|
tar -czf /tmp/warehouse_perms_migration.tar.gz <timestamp>_<name>/
|
||||||
|
scp /tmp/warehouse_perms_migration.tar.gz boha_admin@192.168.50.100:/tmp/
|
||||||
|
|
||||||
|
# On prod
|
||||||
|
ssh boha_admin@192.168.50.100
|
||||||
|
cd /var/www/app-ts/prisma/migrations
|
||||||
|
sudo -u boha_admin tar -xzf /tmp/warehouse_perms_migration.tar.gz
|
||||||
|
cd /var/www/app-ts
|
||||||
|
npx prisma migrate deploy
|
||||||
|
pm2 restart app-ts --update-env
|
||||||
|
```
|
||||||
|
|
||||||
|
The migration is still tracked in git and applied via `prisma migrate
|
||||||
|
deploy` — the only thing the tarball is doing is delivering the
|
||||||
|
`migration.sql` to prod, which is the same mechanism the main release
|
||||||
|
uses. This is preferred over running raw SQL on prod (which the policy
|
||||||
|
in "Database Migrations" forbids).
|
||||||
|
|
||||||
Do not push directly to production or restart services without confirmation.
|
Do not push directly to production or restart services without confirmation.
|
||||||
|
|||||||
450
docs/superpowers/specs/2026-06-05-plan-praci-design.md
Normal file
450
docs/superpowers/specs/2026-06-05-plan-praci-design.md
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
# Plán prací (Work Schedule) Module Design
|
||||||
|
|
||||||
|
**Date:** 2026-06-05
|
||||||
|
**Status:** Approved
|
||||||
|
**Module:** Work schedule / Rozpis prací for boha-app-ts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
A new module under the "Docházka" sidebar section that lets a foreman plan
|
||||||
|
each employee's work on a per-day basis, weeks or months in advance, and lets
|
||||||
|
every employee see what they (and the team) are supposed to be doing on any
|
||||||
|
given day.
|
||||||
|
|
||||||
|
The reference for the UX is `simon/plan_boha.pdf` — a hand-rolled Excel grid
|
||||||
|
with employees as columns and dates as rows, each cell holding a free-text
|
||||||
|
task description. The module replaces that spreadsheet with a live, audited,
|
||||||
|
permission-aware grid in the existing admin app.
|
||||||
|
|
||||||
|
A plan entry is **what should happen**. It is intentionally separate from
|
||||||
|
attendance (which records **what actually happened**). The two are not
|
||||||
|
auto-linked.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Core
|
||||||
|
|
||||||
|
- Plan entries are stored as **date ranges** (e.g. "PLC upgrade VW USA"
|
||||||
|
from 8.6. to 26.6.), with an optional **per-day override** for the rare
|
||||||
|
case where one day in a range is different (e.g. "Volno po noční").
|
||||||
|
- Each entry has:
|
||||||
|
- `user_id` — the employee this plan is for.
|
||||||
|
- `date_from` / `date_to` — inclusive range.
|
||||||
|
- `project_id` — **optional** FK to the existing `projects` table.
|
||||||
|
- `category` — enum (see below), drives color-coding and reports.
|
||||||
|
- `note` — required free-text task description (max 500 chars).
|
||||||
|
- The project dropdown is populated from the existing `projects` table.
|
||||||
|
No new "task" or "sub-project" entity — the note is the human-readable
|
||||||
|
task description.
|
||||||
|
- A cell can be empty (unassigned / not planned). An empty cell is also
|
||||||
|
clickable for admins — opens the create-entry modal.
|
||||||
|
- A `work_plan_overrides` row covers a single `(user, date)` and takes
|
||||||
|
precedence over the range that would otherwise cover that day.
|
||||||
|
|
||||||
|
### Effective-cell resolution (runtime)
|
||||||
|
|
||||||
|
To compute the cell for user `U` on date `D`:
|
||||||
|
|
||||||
|
1. Look up `work_plan_overrides` for `(U, D)`. If present, use it.
|
||||||
|
2. Otherwise, look up `work_plan_entries` where `user_id = U` and
|
||||||
|
`date_from <= D <= date_to`. If multiple match, return the
|
||||||
|
**last-created** one and write a warning to the audit log.
|
||||||
|
3. Otherwise, the cell is empty.
|
||||||
|
|
||||||
|
### Editing rules
|
||||||
|
|
||||||
|
- **Lock past, free future.** Cells with `shift_date < today` are
|
||||||
|
read-only. Admins can override with `?force=1` on the API; the
|
||||||
|
override is recorded in the audit log.
|
||||||
|
- Today and future are always editable for users with `attendance.manage`.
|
||||||
|
- The modal offers three actions when editing a cell inside an existing
|
||||||
|
range:
|
||||||
|
- "Upravit celý rozsah" — edit the range.
|
||||||
|
- "Upravit pouze tento den" — creates an override.
|
||||||
|
- "Zrušit přiřazení tohoto dne" — creates an override with
|
||||||
|
`category = 'other'` and empty note, leaving the cell empty for
|
||||||
|
that day only.
|
||||||
|
|
||||||
|
### Categories
|
||||||
|
|
||||||
|
A new enum `plan_category` for color-coding and reports:
|
||||||
|
|
||||||
|
| Value | Czech label | Cell color hint |
|
||||||
|
| ------------- | -------------- | --------------- |
|
||||||
|
| `work` | Práce | blue |
|
||||||
|
| `preparation` | Příprava | teal |
|
||||||
|
| `travel` | Cesta / Montáž | amber |
|
||||||
|
| `leave` | Dovolená | green |
|
||||||
|
| `sick` | Nemoc | red |
|
||||||
|
| `training` | Školení | purple |
|
||||||
|
| `other` | Jiné | gray |
|
||||||
|
|
||||||
|
Plan overrides for leave/sick are **informational only**. They do not
|
||||||
|
create or modify `leave_requests` or `attendance` rows. Employees still
|
||||||
|
file leave through the existing `Žádosti` flow.
|
||||||
|
|
||||||
|
### Visibility & permissions
|
||||||
|
|
||||||
|
- **Single sidebar entry** under Docházka: `Plán prací` → `/attendance/plan`.
|
||||||
|
- The sidebar entry is gated by `attendance.manage || attendance.record`
|
||||||
|
(mirrors the existing `Žádosti` entry).
|
||||||
|
- The page is **one** (`PlanWork.tsx`):
|
||||||
|
- If `useAuth().hasPermission('attendance.manage')` → editor mode
|
||||||
|
(grid + edit modal + create/edit/delete affordances).
|
||||||
|
- Otherwise → view-only mode (same grid, read-only detail modal).
|
||||||
|
- All endpoints require `attendance.manage` for writes, and either
|
||||||
|
`attendance.manage` or `attendance.record` for reads. Employees
|
||||||
|
with `attendance.record` are scoped server-side to their own
|
||||||
|
`user_id` for any direct row read; the grid endpoint handles the
|
||||||
|
scoping transparently.
|
||||||
|
- Admin role bypasses all permission checks (existing behavior).
|
||||||
|
|
||||||
|
### Grid
|
||||||
|
|
||||||
|
- Default view is **week** (T23, T24, T25 … matches the PDF's week
|
||||||
|
numbering). A toggle at the top switches to **month** view.
|
||||||
|
- The grid is `employees × dates`. Columns are ordered by
|
||||||
|
role/team first, then alphabetically by name.
|
||||||
|
- Columns are users with the `attendance.record` permission (which
|
||||||
|
includes admin). A "Aktivní" toggle in the toolbar lets the planner
|
||||||
|
hide inactive users.
|
||||||
|
- Each cell shows: a colored chip for the category, the project name
|
||||||
|
(if any) in bold, and the note underneath. Empty cells are blank.
|
||||||
|
- Weekend cells have a subtle yellow background tint to match the
|
||||||
|
PDF's "So 6.6. ★" rows.
|
||||||
|
- Read-only employees see the same grid, but cells are non-clickable
|
||||||
|
for editing. A click opens a read-only detail modal showing project,
|
||||||
|
category, note, "Vytvořil: <user> · <date>" (the entry's
|
||||||
|
`created_by`), and (if the cell is part of a range) "Patří do
|
||||||
|
rozsahu: <date_from> – <date_to>".
|
||||||
|
|
||||||
|
### Audit
|
||||||
|
|
||||||
|
Every create/update/delete of `work_plan_entries` and
|
||||||
|
`work_plan_overrides` writes a row to `audit_logs` with
|
||||||
|
`entity_type = 'work_plan_entry' | 'work_plan_override'` and full
|
||||||
|
`old_values` / `new_values` JSON, following the same pattern as
|
||||||
|
`attendance` and `projects`. The `?force=1` past-edit override is
|
||||||
|
recorded in the description field.
|
||||||
|
|
||||||
|
### Non-goals (out of scope for v1)
|
||||||
|
|
||||||
|
- No automatic link to attendance (plan stays independent).
|
||||||
|
- No time-slot granularity (full days only).
|
||||||
|
- No shift / payroll calculation.
|
||||||
|
- No drag-fill on the grid.
|
||||||
|
- No "task list" entity — projects and free text only.
|
||||||
|
- No notification/email when a plan is changed.
|
||||||
|
- No browser tests (the codebase has none; manual verification in dev).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Module placement
|
||||||
|
|
||||||
|
- New module under the **Docházka** sidebar section.
|
||||||
|
- Reuses the existing `attendance.shift_date` concept (date column on
|
||||||
|
the `attendance` table) — the planner lives next to the calendar it
|
||||||
|
informs.
|
||||||
|
- No "administrativa" module exists; the sidebar "Administrativa"
|
||||||
|
section is the commercial back-office (offers, orders, invoices,
|
||||||
|
projects, customers) and is not the right home for HR-style planning.
|
||||||
|
|
||||||
|
### File layout
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── routes/admin/plan.ts # Fastify routes
|
||||||
|
├── services/plan.service.ts # Business logic
|
||||||
|
├── schemas/plan.schema.ts # Zod validation
|
||||||
|
└── __tests__/plan.test.ts # Vitest + Supertest tests
|
||||||
|
|
||||||
|
src/admin/
|
||||||
|
├── pages/PlanWork.tsx # The single page (editor or view-only)
|
||||||
|
├── hooks/usePlanWork.ts # State, modal, mutations
|
||||||
|
├── lib/queries/plan.ts # React Query queryOptions
|
||||||
|
└── components/
|
||||||
|
├── PlanGrid.tsx # Week/month grid (shared by both modes)
|
||||||
|
├── PlanCellModal.tsx # Edit modal (create / edit-range / override)
|
||||||
|
├── PlanCellDetailModal.tsx # Read-only detail modal
|
||||||
|
├── PlanRangeChips.tsx # Cell content chip with category color
|
||||||
|
└── plan.css # Module-specific styles only
|
||||||
|
```
|
||||||
|
|
||||||
|
The new CSS file holds only category color helpers and grid-specific
|
||||||
|
overrides. All form/modal/table styling reuses existing classes
|
||||||
|
(`.admin-table`, `.admin-table-sticky`, `FormField`, `FormModal`,
|
||||||
|
`ConfirmModal`, `AdminDatePicker`, etc.) — no new design tokens.
|
||||||
|
|
||||||
|
### React Query keys
|
||||||
|
|
||||||
|
Per the project's "invalidate the full domain" convention:
|
||||||
|
|
||||||
|
- `['plan', 'grid', { dateFrom, dateTo, view }]`
|
||||||
|
- `['plan', 'entries', { userId, dateFrom, dateTo }]`
|
||||||
|
- `['plan', 'overrides', { userId, dateFrom, dateTo }]`
|
||||||
|
- `['plan', 'users']`
|
||||||
|
|
||||||
|
A mutation on any plan row invalidates `['plan']` (covers all four
|
||||||
|
sub-queries by prefix match).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
### New enum
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
enum plan_category {
|
||||||
|
work
|
||||||
|
preparation
|
||||||
|
travel
|
||||||
|
leave
|
||||||
|
sick
|
||||||
|
training
|
||||||
|
other
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### New table: `work_plan_entries`
|
||||||
|
|
||||||
|
| Column | Type | Notes |
|
||||||
|
| ------------ | ------------------------ | ------------------------------------------------------- |
|
||||||
|
| `id` | int PK autoincrement | |
|
||||||
|
| `user_id` | int FK → `users(id)` | NOT NULL, indexed |
|
||||||
|
| `date_from` | Date | NOT NULL — first day of the range |
|
||||||
|
| `date_to` | Date | NOT NULL — last day of the range, must be ≥ `date_from` |
|
||||||
|
| `project_id` | int? FK → `projects(id)` | nullable — unlinked text entries are allowed |
|
||||||
|
| `category` | enum `plan_category` | NOT NULL, default `'work'` |
|
||||||
|
| `note` | varchar(500) | NOT NULL — free-text task description |
|
||||||
|
| `created_by` | int FK → `users(id)` | who entered it |
|
||||||
|
| `created_at` | DateTime | Prisma default `now()` |
|
||||||
|
| `updated_at` | DateTime | Prisma default `now()` |
|
||||||
|
| `is_deleted` | bool? default false | soft-delete |
|
||||||
|
|
||||||
|
Indexes:
|
||||||
|
|
||||||
|
- `(user_id, date_from)`
|
||||||
|
- `(user_id, date_to)`
|
||||||
|
- `(project_id)`
|
||||||
|
- `(date_from, date_to)` — for cross-user queries like
|
||||||
|
"what's planned for project X this month?"
|
||||||
|
|
||||||
|
### New table: `work_plan_overrides`
|
||||||
|
|
||||||
|
| Column | Type | Notes |
|
||||||
|
| ------------ | ------------------------ | ------------------------------------------------- |
|
||||||
|
| `id` | int PK autoincrement | |
|
||||||
|
| `user_id` | int FK → `users(id)` | NOT NULL, indexed |
|
||||||
|
| `shift_date` | Date | NOT NULL — the one day being overridden |
|
||||||
|
| `project_id` | int? FK → `projects(id)` | nullable |
|
||||||
|
| `category` | enum `plan_category` | NOT NULL — usually `'leave'`, `'sick'`, `'other'` |
|
||||||
|
| `note` | varchar(500) | NOT NULL |
|
||||||
|
| `created_by` | int FK → `users(id)` | |
|
||||||
|
| `created_at` | DateTime | |
|
||||||
|
| `updated_at` | DateTime | |
|
||||||
|
| `is_deleted` | bool? default false | soft-delete |
|
||||||
|
|
||||||
|
Indexes:
|
||||||
|
|
||||||
|
- **Unique** `(user_id, shift_date)` — exactly one non-deleted
|
||||||
|
override per user-day. MySQL unique indexes don't ignore
|
||||||
|
soft-deleted rows, so the service layer enforces "at most one
|
||||||
|
active override per `(user_id, shift_date)`" in application code
|
||||||
|
(the database unique constraint exists as a final safety net and
|
||||||
|
may be temporarily violated by soft-deleted rows, which is
|
||||||
|
acceptable). When restoring a soft-deleted override, the service
|
||||||
|
re-validates that no active override exists for the same day.
|
||||||
|
- `(shift_date)` — for cross-user "what's overridden today" queries.
|
||||||
|
|
||||||
|
### Read semantics (recap)
|
||||||
|
|
||||||
|
For user `U` and date `D`, the "effective" cell is:
|
||||||
|
|
||||||
|
1. Override `(U, D)` if it exists and is not soft-deleted.
|
||||||
|
2. Otherwise the latest `work_plan_entry` whose range covers `D`
|
||||||
|
for user `U` and is not soft-deleted. If multiple match, pick
|
||||||
|
the latest by `created_at` and write a warning to `audit_logs`.
|
||||||
|
3. Otherwise, empty.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Surface
|
||||||
|
|
||||||
|
REST endpoints under `/api/admin/plan`. All routes use the existing
|
||||||
|
Zod-validated, `requirePermission`, `success`/`error` helpers, and
|
||||||
|
`logAudit` pattern. Czech error messages, following project convention.
|
||||||
|
|
||||||
|
| Method | Path | Permission | Body / Query | Returns |
|
||||||
|
| ------ | --------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------- |
|
||||||
|
| GET | `/plan/grid` | `attendance.manage` OR `attendance.record` | `?date_from=&date_to=&view=week\|month` (range ≤ 92 days) | `{ days, users, cells }` — effective cells ready to render |
|
||||||
|
| GET | `/plan/entries` | `attendance.manage` OR `attendance.record` (employee scoped to self) | `?user_id=&date_from=&date_to=` | raw `work_plan_entries` rows in range |
|
||||||
|
| GET | `/plan/overrides` | `attendance.manage` OR `attendance.record` (employee scoped to self) | `?user_id=&date_from=&date_to=` | raw `work_plan_overrides` rows in range |
|
||||||
|
| GET | `/plan/users` | `attendance.manage` OR `attendance.record` | — | users with `attendance.record`, ordered by role/team then name |
|
||||||
|
| POST | `/plan/entries` | `attendance.manage` | `{ user_id, date_from, date_to, project_id?, category, note }` | created entry |
|
||||||
|
| PATCH | `/plan/entries/:id` | `attendance.manage` | partial of create body | updated entry |
|
||||||
|
| DELETE | `/plan/entries/:id` | `attendance.manage` | `?force=1` to bypass past-date lock | `{ ok: true }` (soft-delete) |
|
||||||
|
| POST | `/plan/overrides` | `attendance.manage` | `{ user_id, shift_date, project_id?, category, note }` | created override |
|
||||||
|
| PATCH | `/plan/overrides/:id` | `attendance.manage` | partial of create body | updated override |
|
||||||
|
| DELETE | `/plan/overrides/:id` | `attendance.manage` | `?force=1` | `{ ok: true }` (soft-delete) |
|
||||||
|
| GET | `/plan/audit` | `settings.audit` | `?entity_type=&entity_id=` | audit log entries for a row |
|
||||||
|
|
||||||
|
### Past-date lock
|
||||||
|
|
||||||
|
Server-enforced. POST/PATCH/DELETE with any `shift_date` / `date_from`
|
||||||
|
in the past returns 403 with a Czech error message:
|
||||||
|
|
||||||
|
```
|
||||||
|
Nelze upravovat plán pro datum v minulosti. Pro nouzovou opravu použijte ?force=1.
|
||||||
|
```
|
||||||
|
|
||||||
|
With `?force=1` and `attendance.manage`, the operation succeeds and
|
||||||
|
the audit log records `description = 'force-edit past date'`.
|
||||||
|
|
||||||
|
### Employee scoping
|
||||||
|
|
||||||
|
`GET /plan/entries` and `GET /plan/overrides` accept `?user_id=`. For
|
||||||
|
a user with only `attendance.record`, the service silently overrides
|
||||||
|
the query to scope to `user_id = auth.user.id`. The grid endpoint
|
||||||
|
returns the same data either way (employees see the full grid in
|
||||||
|
view-only mode).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend Detail
|
||||||
|
|
||||||
|
### `PlanWork.tsx`
|
||||||
|
|
||||||
|
- Mounts and calls `useAuth()`. If `hasPermission('attendance.manage')`
|
||||||
|
→ editor state. Otherwise → view-only state.
|
||||||
|
- Renders the toolbar (view toggle, date navigator, active-toggle,
|
||||||
|
print) and `PlanGrid`.
|
||||||
|
- Owns the modal state machine for `PlanCellModal` (editor) or
|
||||||
|
`PlanCellDetailModal` (view).
|
||||||
|
|
||||||
|
### `PlanGrid.tsx`
|
||||||
|
|
||||||
|
- Pure presentational. Takes `users`, `days`, `cells` as props.
|
||||||
|
- Renders a `<table>` with sticky first column (date) and sticky
|
||||||
|
header (employee names).
|
||||||
|
- Each cell is a `<button>` (admin) or `<div>` (view-only) with the
|
||||||
|
colored chip + note.
|
||||||
|
- Calls `onCellClick(userId, date)` for the parent to open the modal.
|
||||||
|
- Reuses `.admin-table` and `.admin-table-sticky` from existing
|
||||||
|
stylesheets.
|
||||||
|
|
||||||
|
### `PlanCellModal.tsx` (editor)
|
||||||
|
|
||||||
|
Three modes, decided by what the user clicked:
|
||||||
|
|
||||||
|
- **Create** — empty cell. Fields: `user_id` (locked to clicked user,
|
||||||
|
can be switched), `date_from` (default = clicked date), `date_to`
|
||||||
|
(default = clicked date; "Jeden den" / "Rozsah dnů" toggle),
|
||||||
|
`project_id` (dropdown, populated from `projects`, nullable),
|
||||||
|
`category` (select), `note` (textarea, required).
|
||||||
|
- **Edit range** — cell is part of an existing range. Loads the
|
||||||
|
full range into the form. "Upravit celý rozsah" button is the
|
||||||
|
primary action.
|
||||||
|
- **Edit day in range** — cell is part of a range. Three buttons:
|
||||||
|
"Upravit celý rozsah" / "Upravit pouze tento den" /
|
||||||
|
"Zrušit přiřazení tohoto dne".
|
||||||
|
- **Edit single-day override** — cell is itself a `work_plan_overrides`
|
||||||
|
row. Standard edit form.
|
||||||
|
- Bottom: delete button (with `ConfirmModal`).
|
||||||
|
|
||||||
|
### `PlanCellDetailModal.tsx` (view-only)
|
||||||
|
|
||||||
|
Read-only. Shows project, category, note, "Vytvořil: <full name of
|
||||||
|
`created_by`> · <`created_at` in local Czech time>", and (if the
|
||||||
|
cell is part of a range) "Patří do rozsahu: <date_from> – <date_to>".
|
||||||
|
For override cells, the "Vytvořil" line refers to the override's own
|
||||||
|
`created_by` (not the parent range's). No edit, no delete.
|
||||||
|
|
||||||
|
### `usePlanWork.ts`
|
||||||
|
|
||||||
|
Owns:
|
||||||
|
|
||||||
|
- `view: 'week' | 'month'`, `anchorDate`, `filterActive`
|
||||||
|
- `effectiveCells` from `['plan', 'grid']` query
|
||||||
|
- `entries` and `overrides` for the cell being edited
|
||||||
|
- `modalState: { mode, userId, date, entryId?, overrideId? }`
|
||||||
|
- `createEntry`, `updateEntry`, `deleteEntry`,
|
||||||
|
`createOverride`, `updateOverride`, `deleteOverride` —
|
||||||
|
React Query mutations that invalidate `['plan']`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration
|
||||||
|
|
||||||
|
Single new migration:
|
||||||
|
|
||||||
|
`prisma/migrations/<timestamp>_add_work_plan/`
|
||||||
|
|
||||||
|
- `migration.sql`:
|
||||||
|
1. `CREATE TABLE work_plan_entries (...)` with columns and indexes.
|
||||||
|
2. `CREATE TABLE work_plan_overrides (...)` with columns and indexes.
|
||||||
|
3. `CREATE TYPE plan_category` (or in-table enum, per Prisma's
|
||||||
|
MySQL output).
|
||||||
|
|
||||||
|
No data migration. No `seed.ts` change. No new permission keys.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
`src/__tests__/plan.test.ts` — Vitest + Supertest against the real
|
||||||
|
test database (`.env.test`):
|
||||||
|
|
||||||
|
1. **Effective-cell resolution** — range + override returns the
|
||||||
|
override for that day, the range for the other days, `null`
|
||||||
|
for uncovered days.
|
||||||
|
2. **Past-date lock** — POST with `date_from < today` returns 403
|
||||||
|
for users without `attendance.manage`; allowed with `?force=1`
|
||||||
|
for users with `attendance.manage`; audit log records the force.
|
||||||
|
3. **Employee self-view scoping** — employee with `attendance.record`
|
||||||
|
calling `GET /plan/entries?user_id=<other>` is scoped to self.
|
||||||
|
4. **Soft-delete + is_deleted** — deleted rows are excluded from
|
||||||
|
`/plan/grid` but kept in `/plan/audit`.
|
||||||
|
5. **Project FK nullability** — entries with `project_id = null`
|
||||||
|
and `category = 'leave'` are valid and render correctly.
|
||||||
|
6. **Range overlap** — two ranges for the same `(user, day)` does
|
||||||
|
not error; the latest one wins; a warning is logged.
|
||||||
|
7. **Audit log on every write** — POST/PATCH/DELETE each insert
|
||||||
|
exactly one `audit_logs` row with `entity_type`,
|
||||||
|
`old_values`, `new_values`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Release Plan
|
||||||
|
|
||||||
|
1. Bump `package.json` version (minor, since new module).
|
||||||
|
2. `npm run build` — confirm clean compile.
|
||||||
|
3. Commit + push to Gitea.
|
||||||
|
4. Tarball + deploy to production via the existing release script
|
||||||
|
(per `CLAUDE.md` "Release Process" section).
|
||||||
|
5. `npx prisma migrate deploy` on prod.
|
||||||
|
6. `pm2 restart app-ts --update-env`.
|
||||||
|
7. Smoke test: log in, open Docházka → Plán prací, add a one-day
|
||||||
|
entry for yourself, reload, confirm it shows.
|
||||||
|
|
||||||
|
The deployment follows the same pattern as the warehouse module
|
||||||
|
release (see commit `5233db2` and the `2026-05-29-warehouse-module`
|
||||||
|
spec for reference).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open items / future work
|
||||||
|
|
||||||
|
- Month view polish if the toggle proves popular.
|
||||||
|
- Drag-fill on the grid (out of scope for v1).
|
||||||
|
- Optional tie-in to attendance: pre-fill `notes` on
|
||||||
|
`AttendanceCreate` from today's plan (read-only hint). Not
|
||||||
|
pursued in v1.
|
||||||
|
- "Patří do rozsahu" link in the detail modal: clickable
|
||||||
|
jump to the range's full detail.
|
||||||
|
- Per-team filtering (if a "team" concept is added to `users` later).
|
||||||
572
package-lock.json
generated
572
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "app-ts",
|
"name": "app-ts",
|
||||||
"version": "1.6.5",
|
"version": "1.9.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "dist/server.js",
|
"main": "dist/server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -18,7 +18,6 @@
|
|||||||
"db:studio": "prisma studio",
|
"db:studio": "prisma studio",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"bones": "boneyard-js build http://localhost:3000",
|
|
||||||
"seed": "tsx prisma/seed.ts"
|
"seed": "tsx prisma/seed.ts"
|
||||||
},
|
},
|
||||||
"prisma": {
|
"prisma": {
|
||||||
@@ -42,7 +41,6 @@
|
|||||||
"@tanstack/react-query": "^5.100.5",
|
"@tanstack/react-query": "^5.100.5",
|
||||||
"@types/jsdom": "^28.0.1",
|
"@types/jsdom": "^28.0.1",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"boneyard-js": "^1.8.1",
|
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dompurify": "^3.3.3",
|
"dompurify": "^3.3.3",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Insert the settings.system permission (also available via seed)
|
||||||
|
INSERT INTO `permissions` (`name`, `display_name`, `module`, `description`)
|
||||||
|
VALUES ('settings.system', 'Systémová nastavení', 'settings', 'Spravovat systémová nastavení, 2FA, e-maily, limity')
|
||||||
|
ON DUPLICATE KEY UPDATE `name` = `name`;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `quotations` DROP COLUMN `exchange_rate`;
|
||||||
|
ALTER TABLE `quotations` DROP COLUMN `exchange_rate_date`;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `invoice_items` ADD COLUMN `item_description` TEXT NULL;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
-- AlterTable: quotation_items
|
||||||
|
ALTER TABLE `quotation_items` DROP COLUMN `uuid`;
|
||||||
|
ALTER TABLE `quotation_items` DROP COLUMN `is_deleted`;
|
||||||
|
ALTER TABLE `quotation_items` DROP COLUMN `sync_version`;
|
||||||
|
|
||||||
|
-- AlterTable: quotations
|
||||||
|
ALTER TABLE `quotations` DROP COLUMN `uuid`;
|
||||||
|
ALTER TABLE `quotations` DROP COLUMN `sync_version`;
|
||||||
|
|
||||||
|
-- AlterTable: scope_sections
|
||||||
|
ALTER TABLE `scope_sections` DROP COLUMN `content_editor_height`;
|
||||||
|
ALTER TABLE `scope_sections` DROP COLUMN `uuid`;
|
||||||
|
ALTER TABLE `scope_sections` DROP COLUMN `is_deleted`;
|
||||||
|
ALTER TABLE `scope_sections` DROP COLUMN `sync_version`;
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `sklad_categories` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` VARCHAR(100) NOT NULL,
|
||||||
|
`description` TEXT NULL,
|
||||||
|
`sort_order` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`created_at` DATETIME(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
|
||||||
|
`modified_at` DATETIME(0) NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `sklad_suppliers` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` VARCHAR(255) NOT NULL,
|
||||||
|
`ico` VARCHAR(20) NULL,
|
||||||
|
`dic` VARCHAR(20) NULL,
|
||||||
|
`contact_person` VARCHAR(255) NULL,
|
||||||
|
`email` VARCHAR(255) NULL,
|
||||||
|
`phone` VARCHAR(50) NULL,
|
||||||
|
`address` TEXT NULL,
|
||||||
|
`notes` TEXT NULL,
|
||||||
|
`is_active` BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
`created_at` DATETIME(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
|
||||||
|
`modified_at` DATETIME(0) NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `sklad_locations` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`code` VARCHAR(20) NOT NULL,
|
||||||
|
`name` VARCHAR(100) NOT NULL,
|
||||||
|
`description` TEXT NULL,
|
||||||
|
`is_active` BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
`created_at` DATETIME(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
|
||||||
|
`modified_at` DATETIME(0) NULL,
|
||||||
|
|
||||||
|
UNIQUE INDEX `sklad_locations_code_key`(`code`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `sklad_items` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`item_number` VARCHAR(50) NULL,
|
||||||
|
`name` VARCHAR(255) NOT NULL,
|
||||||
|
`description` TEXT NULL,
|
||||||
|
`category_id` INTEGER NULL,
|
||||||
|
`unit` VARCHAR(20) NOT NULL,
|
||||||
|
`min_quantity` DECIMAL(12, 3) NULL,
|
||||||
|
`is_active` BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
`notes` TEXT NULL,
|
||||||
|
`created_at` DATETIME(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
|
||||||
|
`modified_at` DATETIME(0) NULL,
|
||||||
|
|
||||||
|
UNIQUE INDEX `sklad_items_item_number_key`(`item_number`),
|
||||||
|
INDEX `sklad_items_category_id`(`category_id`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `sklad_batches` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`item_id` INTEGER NOT NULL,
|
||||||
|
`receipt_line_id` INTEGER NOT NULL,
|
||||||
|
`quantity` DECIMAL(12, 3) NOT NULL,
|
||||||
|
`original_qty` DECIMAL(12, 3) NOT NULL,
|
||||||
|
`unit_price` DECIMAL(12, 2) NOT NULL,
|
||||||
|
`received_at` DATETIME(0) NULL,
|
||||||
|
`is_consumed` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
|
||||||
|
UNIQUE INDEX `sklad_batches_receipt_line_id_key`(`receipt_line_id`),
|
||||||
|
INDEX `sklad_batches_fifo`(`item_id`, `is_consumed`, `received_at`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `sklad_item_locations` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`item_id` INTEGER NOT NULL,
|
||||||
|
`location_id` INTEGER NOT NULL,
|
||||||
|
`quantity` DECIMAL(12, 3) NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
UNIQUE INDEX `sklad_item_locations_unique`(`item_id`, `location_id`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `sklad_receipts` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`receipt_number` VARCHAR(50) NULL,
|
||||||
|
`supplier_id` INTEGER NULL,
|
||||||
|
`delivery_note_number` VARCHAR(100) NULL,
|
||||||
|
`delivery_note_date` DATETIME(0) NULL,
|
||||||
|
`received_by` INTEGER NULL,
|
||||||
|
`notes` TEXT NULL,
|
||||||
|
`status` ENUM('DRAFT', 'CONFIRMED', 'CANCELLED') NOT NULL DEFAULT 'DRAFT',
|
||||||
|
`created_at` DATETIME(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
|
||||||
|
`modified_at` DATETIME(0) NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `sklad_receipt_lines` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`receipt_id` INTEGER NOT NULL,
|
||||||
|
`item_id` INTEGER NOT NULL,
|
||||||
|
`quantity` DECIMAL(12, 3) NOT NULL,
|
||||||
|
`unit_price` DECIMAL(12, 2) NOT NULL,
|
||||||
|
`location_id` INTEGER NULL,
|
||||||
|
`notes` VARCHAR(255) NULL,
|
||||||
|
|
||||||
|
INDEX `sklad_receipt_lines_receipt_id`(`receipt_id`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `sklad_receipt_attachments` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`receipt_id` INTEGER NOT NULL,
|
||||||
|
`file_name` VARCHAR(255) NOT NULL,
|
||||||
|
`file_mime` VARCHAR(100) NOT NULL,
|
||||||
|
`file_size` INTEGER NOT NULL,
|
||||||
|
`file_path` VARCHAR(500) NOT NULL,
|
||||||
|
`created_at` DATETIME(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
|
||||||
|
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `sklad_issues` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`issue_number` VARCHAR(50) NULL,
|
||||||
|
`project_id` INTEGER NOT NULL,
|
||||||
|
`issued_by` INTEGER NULL,
|
||||||
|
`notes` TEXT NULL,
|
||||||
|
`status` ENUM('DRAFT', 'CONFIRMED', 'CANCELLED') NOT NULL DEFAULT 'DRAFT',
|
||||||
|
`created_at` DATETIME(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
|
||||||
|
`modified_at` DATETIME(0) NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `sklad_issue_lines` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`issue_id` INTEGER NOT NULL,
|
||||||
|
`item_id` INTEGER NOT NULL,
|
||||||
|
`batch_id` INTEGER NOT NULL,
|
||||||
|
`quantity` DECIMAL(12, 3) NOT NULL,
|
||||||
|
`location_id` INTEGER NULL,
|
||||||
|
`reservation_id` INTEGER NULL,
|
||||||
|
`notes` VARCHAR(255) NULL,
|
||||||
|
|
||||||
|
INDEX `sklad_issue_lines_issue_id`(`issue_id`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `sklad_reservations` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`item_id` INTEGER NOT NULL,
|
||||||
|
`project_id` INTEGER NOT NULL,
|
||||||
|
`quantity` DECIMAL(12, 3) NOT NULL,
|
||||||
|
`remaining_qty` DECIMAL(12, 3) NOT NULL,
|
||||||
|
`reserved_by` INTEGER NULL,
|
||||||
|
`notes` TEXT NULL,
|
||||||
|
`status` ENUM('ACTIVE', 'FULFILLED', 'CANCELLED') NOT NULL DEFAULT 'ACTIVE',
|
||||||
|
`created_at` DATETIME(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
|
||||||
|
`modified_at` DATETIME(0) NULL,
|
||||||
|
|
||||||
|
INDEX `sklad_reservations_item_status`(`item_id`, `status`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `sklad_inventory_sessions` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`session_number` VARCHAR(50) NULL,
|
||||||
|
`notes` TEXT NULL,
|
||||||
|
`status` ENUM('DRAFT', 'CONFIRMED') NOT NULL DEFAULT 'DRAFT',
|
||||||
|
`created_at` DATETIME(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
|
||||||
|
`modified_at` DATETIME(0) NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `sklad_inventory_lines` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`session_id` INTEGER NOT NULL,
|
||||||
|
`item_id` INTEGER NOT NULL,
|
||||||
|
`location_id` INTEGER NULL,
|
||||||
|
`system_qty` DECIMAL(12, 3) NOT NULL,
|
||||||
|
`actual_qty` DECIMAL(12, 3) NOT NULL,
|
||||||
|
`difference` DECIMAL(12, 3) NOT NULL,
|
||||||
|
`notes` VARCHAR(255) NULL,
|
||||||
|
|
||||||
|
INDEX `sklad_inventory_lines_session_id`(`session_id`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `sklad_items` ADD CONSTRAINT `sklad_items_category_id_fkey` FOREIGN KEY (`category_id`) REFERENCES `sklad_categories`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `sklad_batches` ADD CONSTRAINT `sklad_batches_item_id_fkey` FOREIGN KEY (`item_id`) REFERENCES `sklad_items`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `sklad_batches` ADD CONSTRAINT `sklad_batches_receipt_line_id_fkey` FOREIGN KEY (`receipt_line_id`) REFERENCES `sklad_receipt_lines`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `sklad_item_locations` ADD CONSTRAINT `sklad_item_locations_item_id_fkey` FOREIGN KEY (`item_id`) REFERENCES `sklad_items`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `sklad_item_locations` ADD CONSTRAINT `sklad_item_locations_location_id_fkey` FOREIGN KEY (`location_id`) REFERENCES `sklad_locations`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `sklad_receipts` ADD CONSTRAINT `sklad_receipts_supplier_id_fkey` FOREIGN KEY (`supplier_id`) REFERENCES `sklad_suppliers`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `sklad_receipts` ADD CONSTRAINT `sklad_receipts_received_by_fkey` FOREIGN KEY (`received_by`) REFERENCES `users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `sklad_receipt_lines` ADD CONSTRAINT `sklad_receipt_lines_receipt_id_fkey` FOREIGN KEY (`receipt_id`) REFERENCES `sklad_receipts`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `sklad_receipt_lines` ADD CONSTRAINT `sklad_receipt_lines_item_id_fkey` FOREIGN KEY (`item_id`) REFERENCES `sklad_items`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `sklad_receipt_lines` ADD CONSTRAINT `sklad_receipt_lines_location_id_fkey` FOREIGN KEY (`location_id`) REFERENCES `sklad_locations`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `sklad_receipt_attachments` ADD CONSTRAINT `sklad_receipt_attachments_receipt_id_fkey` FOREIGN KEY (`receipt_id`) REFERENCES `sklad_receipts`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `sklad_issues` ADD CONSTRAINT `sklad_issues_project_id_fkey` FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `sklad_issues` ADD CONSTRAINT `sklad_issues_issued_by_fkey` FOREIGN KEY (`issued_by`) REFERENCES `users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `sklad_issue_lines` ADD CONSTRAINT `sklad_issue_lines_issue_id_fkey` FOREIGN KEY (`issue_id`) REFERENCES `sklad_issues`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `sklad_issue_lines` ADD CONSTRAINT `sklad_issue_lines_item_id_fkey` FOREIGN KEY (`item_id`) REFERENCES `sklad_items`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `sklad_issue_lines` ADD CONSTRAINT `sklad_issue_lines_batch_id_fkey` FOREIGN KEY (`batch_id`) REFERENCES `sklad_batches`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `sklad_issue_lines` ADD CONSTRAINT `sklad_issue_lines_location_id_fkey` FOREIGN KEY (`location_id`) REFERENCES `sklad_locations`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `sklad_issue_lines` ADD CONSTRAINT `sklad_issue_lines_reservation_id_fkey` FOREIGN KEY (`reservation_id`) REFERENCES `sklad_reservations`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `sklad_reservations` ADD CONSTRAINT `sklad_reservations_item_id_fkey` FOREIGN KEY (`item_id`) REFERENCES `sklad_items`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `sklad_reservations` ADD CONSTRAINT `sklad_reservations_project_id_fkey` FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `sklad_reservations` ADD CONSTRAINT `sklad_reservations_reserved_by_fkey` FOREIGN KEY (`reserved_by`) REFERENCES `users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `sklad_inventory_lines` ADD CONSTRAINT `sklad_inventory_lines_session_id_fkey` FOREIGN KEY (`session_id`) REFERENCES `sklad_inventory_sessions`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `sklad_inventory_lines` ADD CONSTRAINT `sklad_inventory_lines_item_id_fkey` FOREIGN KEY (`item_id`) REFERENCES `sklad_items`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `sklad_inventory_lines` ADD CONSTRAINT `sklad_inventory_lines_location_id_fkey` FOREIGN KEY (`location_id`) REFERENCES `sklad_locations`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `company_settings` ADD COLUMN `warehouse_inventory_number_pattern` VARCHAR(100) NULL,
|
||||||
|
ADD COLUMN `warehouse_inventory_prefix` VARCHAR(20) NULL,
|
||||||
|
ADD COLUMN `warehouse_issue_number_pattern` VARCHAR(100) NULL,
|
||||||
|
ADD COLUMN `warehouse_issue_prefix` VARCHAR(20) NULL,
|
||||||
|
ADD COLUMN `warehouse_receipt_number_pattern` VARCHAR(100) NULL,
|
||||||
|
ADD COLUMN `warehouse_receipt_prefix` VARCHAR(20) NULL;
|
||||||
|
|
||||||
|
-- Set backward-compatible defaults for existing installations
|
||||||
|
UPDATE `company_settings`
|
||||||
|
SET
|
||||||
|
`warehouse_receipt_prefix` = 'PRI',
|
||||||
|
`warehouse_receipt_number_pattern` = '{PREFIX}-{YYYY}-{NNN}',
|
||||||
|
`warehouse_issue_prefix` = 'VYD',
|
||||||
|
`warehouse_issue_number_pattern` = '{PREFIX}-{YYYY}-{NNN}',
|
||||||
|
`warehouse_inventory_prefix` = 'INV',
|
||||||
|
`warehouse_inventory_number_pattern` = '{PREFIX}-{YYYY}-{NNN}';
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
-- Add warehouse module permissions and assign them to the admin role.
|
||||||
|
-- These rows used to be created by `prisma/seed.ts`, which is dev-only and
|
||||||
|
-- must not be run against production (per CLAUDE.md). This migration is
|
||||||
|
-- idempotent: re-running it is a no-op.
|
||||||
|
--
|
||||||
|
-- The 4 permissions match `prisma/seed.ts` lines 284-308 exactly.
|
||||||
|
|
||||||
|
-- 1. Insert the 4 warehouse permissions (INSERT IGNORE skips on duplicate
|
||||||
|
-- name, so re-running the migration is safe).
|
||||||
|
INSERT IGNORE INTO `permissions` (`name`, `display_name`, `module`, `description`, `created_at`) VALUES
|
||||||
|
('warehouse.view', 'Zobrazit sklad', 'warehouse', 'Prohlížet stav skladu, položky, reporty a historii pohybů', NOW()),
|
||||||
|
('warehouse.operate', 'Příjem a výdej', 'warehouse', 'Vytvářet a potvrzovat příjmy, výdeje a rezervace', NOW()),
|
||||||
|
('warehouse.manage', 'Správa skladu', 'warehouse', 'Spravovat katalog materiálů, dodavatele, lokace a kategorie', NOW()),
|
||||||
|
('warehouse.inventory', 'Inventura', 'warehouse', 'Vytvářet a potvrzovat inventurní sčítkání', NOW());
|
||||||
|
|
||||||
|
-- 2. Assign all 4 permissions to the admin role. INSERT IGNORE on the
|
||||||
|
-- composite primary key (role_id, permission_id) makes this idempotent
|
||||||
|
-- and preserves any other role assignments that already exist.
|
||||||
|
INSERT IGNORE INTO `role_permissions` (`role_id`, `permission_id`)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM `roles` r
|
||||||
|
CROSS JOIN `permissions` p
|
||||||
|
WHERE r.name = 'admin'
|
||||||
|
AND p.name IN (
|
||||||
|
'warehouse.view',
|
||||||
|
'warehouse.operate',
|
||||||
|
'warehouse.manage',
|
||||||
|
'warehouse.inventory'
|
||||||
|
);
|
||||||
61
prisma/migrations/20260605120000_add_work_plan/migration.sql
Normal file
61
prisma/migrations/20260605120000_add_work_plan/migration.sql
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
-- Add work_plan_entries and work_plan_overrides tables plus the plan_category
|
||||||
|
-- enum. Both tables are owned by users (Cascade on delete) and reference
|
||||||
|
-- projects (SetNull). `created_by` uses Restrict so we never silently lose
|
||||||
|
-- the audit trail of who created the entry.
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `work_plan_entries` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` INTEGER NOT NULL,
|
||||||
|
`date_from` DATE NOT NULL,
|
||||||
|
`date_to` DATE NOT NULL,
|
||||||
|
`project_id` INTEGER NULL,
|
||||||
|
`category` ENUM('work', 'preparation', 'travel', 'leave', 'sick', 'training', 'other') NOT NULL DEFAULT 'work',
|
||||||
|
`note` VARCHAR(500) NOT NULL,
|
||||||
|
`created_by` INTEGER NOT NULL,
|
||||||
|
`created_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
|
||||||
|
`updated_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
|
||||||
|
`is_deleted` BOOLEAN NULL DEFAULT false,
|
||||||
|
|
||||||
|
INDEX `idx_wpe_user_from`(`user_id`, `date_from`),
|
||||||
|
INDEX `idx_wpe_user_to`(`user_id`, `date_to`),
|
||||||
|
INDEX `idx_wpe_project`(`project_id`),
|
||||||
|
INDEX `idx_wpe_dates`(`date_from`, `date_to`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `work_plan_overrides` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` INTEGER NOT NULL,
|
||||||
|
`shift_date` DATE NOT NULL,
|
||||||
|
`project_id` INTEGER NULL,
|
||||||
|
`category` ENUM('work', 'preparation', 'travel', 'leave', 'sick', 'training', 'other') NOT NULL,
|
||||||
|
`note` VARCHAR(500) NOT NULL,
|
||||||
|
`created_by` INTEGER NOT NULL,
|
||||||
|
`created_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
|
||||||
|
`updated_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
|
||||||
|
`is_deleted` BOOLEAN NULL DEFAULT false,
|
||||||
|
|
||||||
|
INDEX `idx_wpo_date`(`shift_date`),
|
||||||
|
UNIQUE INDEX `uniq_wpo_user_date`(`user_id`, `shift_date`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `work_plan_entries` ADD CONSTRAINT `work_plan_entries_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `work_plan_entries` ADD CONSTRAINT `work_plan_entries_project_id_fkey` FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON DELETE SET NULL ON UPDATE NO ACTION;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `work_plan_entries` ADD CONSTRAINT `work_plan_entries_created_by_fkey` FOREIGN KEY (`created_by`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE NO ACTION;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `work_plan_overrides` ADD CONSTRAINT `work_plan_overrides_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `work_plan_overrides` ADD CONSTRAINT `work_plan_overrides_project_id_fkey` FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON DELETE SET NULL ON UPDATE NO ACTION;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `work_plan_overrides` ADD CONSTRAINT `work_plan_overrides_created_by_fkey` FOREIGN KEY (`created_by`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE NO ACTION;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
-- Drop the unique index on (user_id, shift_date) for work_plan_overrides.
|
||||||
|
-- MySQL unique indexes don't ignore soft-deleted rows, which would prevent
|
||||||
|
-- createOverride from soft-deleting an existing override and creating a new
|
||||||
|
-- one for the same (user_id, shift_date). Application code enforces the
|
||||||
|
-- "at most one active override per (user_id, shift_date)" invariant.
|
||||||
|
-- The application-level check is wrapped in a transaction; see
|
||||||
|
-- src/services/plan.service.ts:createOverride.
|
||||||
|
--
|
||||||
|
-- The original index was created in
|
||||||
|
-- 20260605120000_add_work_plan/migration.sql as `uniq_wpo_user_date`.
|
||||||
|
|
||||||
|
-- IMPORTANT ordering: `uniq_wpo_user_date` is the only index covering the
|
||||||
|
-- `user_id` foreign key, and MySQL refuses to drop an index still needed by a
|
||||||
|
-- FK. So we CREATE the replacement non-unique index FIRST (giving the FK
|
||||||
|
-- another covering index), THEN drop the unique one. The reverse order fails
|
||||||
|
-- with "Cannot drop index ... needed in a foreign key constraint" on a fresh
|
||||||
|
-- apply.
|
||||||
|
|
||||||
|
-- Add a non-unique index for the (user_id, shift_date) lookup used by
|
||||||
|
-- createOverride's "is there an active override for this user-day?" query
|
||||||
|
-- and by resolveCell / resolveGrid. This is a plain index, not a unique
|
||||||
|
-- one — multiple rows (active and soft-deleted) can share the same
|
||||||
|
-- (user_id, shift_date) pair.
|
||||||
|
CREATE INDEX `idx_wpo_user_date` ON `work_plan_overrides`(`user_id`, `shift_date`);
|
||||||
|
|
||||||
|
DROP INDEX `uniq_wpo_user_date` ON `work_plan_overrides`;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
-- Add plan_categories table and convert category columns from ENUM to VARCHAR(50)
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `plan_categories` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`key` VARCHAR(50) NOT NULL,
|
||||||
|
`label` VARCHAR(100) NOT NULL,
|
||||||
|
`color` VARCHAR(7) NOT NULL,
|
||||||
|
`sort_order` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`is_active` BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
`created_at` DATETIME(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
|
||||||
|
`updated_at` DATETIME(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
|
||||||
|
|
||||||
|
UNIQUE INDEX `plan_categories_key_key`(`key`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- AlterTable: change category from ENUM to VARCHAR(50) in work_plan_entries
|
||||||
|
ALTER TABLE `work_plan_entries` MODIFY `category` VARCHAR(50) NOT NULL DEFAULT 'work';
|
||||||
|
|
||||||
|
-- AlterTable: change category from ENUM to VARCHAR(50) in work_plan_overrides
|
||||||
|
ALTER TABLE `work_plan_overrides` MODIFY `category` VARCHAR(50) NOT NULL;
|
||||||
|
|
||||||
|
-- Seed the categories that were previously the plan_category enum
|
||||||
|
INSERT INTO `plan_categories` (`key`, `label`, `color`, `sort_order`, `is_active`) VALUES
|
||||||
|
('work', 'Práce', '#2563eb', 1, true),
|
||||||
|
('preparation', 'Příprava', '#0d9488', 2, true),
|
||||||
|
('travel', 'Cesta / Montáž', '#ca8a04', 3, true),
|
||||||
|
('leave', 'Dovolená', '#16a34a', 4, true),
|
||||||
|
('sick', 'Nemoc', '#dc2626', 5, true),
|
||||||
|
('training', 'Školení', '#7c3aed', 6, true),
|
||||||
|
('other', 'Jiné', '#6b7280', 7, true);
|
||||||
@@ -128,6 +128,12 @@ model company_settings {
|
|||||||
offer_number_pattern String? @db.VarChar(100)
|
offer_number_pattern String? @db.VarChar(100)
|
||||||
order_number_pattern String? @db.VarChar(100)
|
order_number_pattern String? @db.VarChar(100)
|
||||||
invoice_number_pattern String? @db.VarChar(100)
|
invoice_number_pattern String? @db.VarChar(100)
|
||||||
|
warehouse_receipt_prefix String? @db.VarChar(20)
|
||||||
|
warehouse_receipt_number_pattern String? @db.VarChar(100)
|
||||||
|
warehouse_issue_prefix String? @db.VarChar(20)
|
||||||
|
warehouse_issue_number_pattern String? @db.VarChar(100)
|
||||||
|
warehouse_inventory_prefix String? @db.VarChar(20)
|
||||||
|
warehouse_inventory_number_pattern String? @db.VarChar(100)
|
||||||
}
|
}
|
||||||
|
|
||||||
model customers {
|
model customers {
|
||||||
@@ -154,6 +160,7 @@ model invoice_items {
|
|||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
invoice_id Int
|
invoice_id Int
|
||||||
description String? @db.VarChar(500)
|
description String? @db.VarChar(500)
|
||||||
|
item_description String? @db.Text
|
||||||
quantity Decimal? @default(1.000) @db.Decimal(12, 3)
|
quantity Decimal? @default(1.000) @db.Decimal(12, 3)
|
||||||
unit String? @db.VarChar(20)
|
unit String? @db.VarChar(20)
|
||||||
unit_price Decimal? @default(0.00) @db.Decimal(12, 2)
|
unit_price Decimal? @default(0.00) @db.Decimal(12, 2)
|
||||||
@@ -356,6 +363,10 @@ model projects {
|
|||||||
modified_at DateTime? @db.DateTime(0)
|
modified_at DateTime? @db.DateTime(0)
|
||||||
attendance_project_logs attendance_project_logs[]
|
attendance_project_logs attendance_project_logs[]
|
||||||
project_notes project_notes[]
|
project_notes project_notes[]
|
||||||
|
sklad_issues sklad_issues[]
|
||||||
|
sklad_reservations sklad_reservations[]
|
||||||
|
work_plan_entries work_plan_entries[]
|
||||||
|
work_plan_overrides work_plan_overrides[]
|
||||||
users users? @relation(fields: [responsible_user_id], references: [id], onUpdate: NoAction, map: "fk_projects_responsible_user")
|
users users? @relation(fields: [responsible_user_id], references: [id], onUpdate: NoAction, map: "fk_projects_responsible_user")
|
||||||
customers customers? @relation(fields: [customer_id], references: [id], onUpdate: NoAction, map: "projects_ibfk_1")
|
customers customers? @relation(fields: [customer_id], references: [id], onUpdate: NoAction, map: "projects_ibfk_1")
|
||||||
quotations quotations? @relation(fields: [quotation_id], references: [id], onUpdate: NoAction, map: "projects_ibfk_2")
|
quotations quotations? @relation(fields: [quotation_id], references: [id], onUpdate: NoAction, map: "projects_ibfk_2")
|
||||||
@@ -377,10 +388,7 @@ model quotation_items {
|
|||||||
unit String? @db.VarChar(20)
|
unit String? @db.VarChar(20)
|
||||||
unit_price Decimal? @default(0.00) @db.Decimal(12, 2)
|
unit_price Decimal? @default(0.00) @db.Decimal(12, 2)
|
||||||
is_included_in_total Boolean? @default(true)
|
is_included_in_total Boolean? @default(true)
|
||||||
uuid String? @db.VarChar(36)
|
|
||||||
modified_at DateTime? @db.DateTime(0)
|
modified_at DateTime? @db.DateTime(0)
|
||||||
is_deleted Boolean? @default(false)
|
|
||||||
sync_version Int? @default(0)
|
|
||||||
quotations quotations @relation(fields: [quotation_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "quotation_items_ibfk_1")
|
quotations quotations @relation(fields: [quotation_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "quotation_items_ibfk_1")
|
||||||
|
|
||||||
@@index([quotation_id], map: "quotation_id")
|
@@index([quotation_id], map: "quotation_id")
|
||||||
@@ -397,17 +405,13 @@ model quotations {
|
|||||||
language String? @default("cs") @db.VarChar(5)
|
language String? @default("cs") @db.VarChar(5)
|
||||||
vat_rate Decimal? @default(21.00) @db.Decimal(5, 2)
|
vat_rate Decimal? @default(21.00) @db.Decimal(5, 2)
|
||||||
apply_vat Boolean? @default(true)
|
apply_vat Boolean? @default(true)
|
||||||
exchange_rate Decimal? @default(1.0000) @db.Decimal(10, 4)
|
|
||||||
exchange_rate_date DateTime? @db.Date
|
|
||||||
order_id Int?
|
order_id Int?
|
||||||
status String @default("active") @db.VarChar(20)
|
status String @default("active") @db.VarChar(20)
|
||||||
scope_title String? @db.VarChar(500)
|
scope_title String? @db.VarChar(500)
|
||||||
scope_description String? @db.Text
|
scope_description String? @db.Text
|
||||||
locked_by Int?
|
locked_by Int?
|
||||||
locked_at DateTime? @db.DateTime(0)
|
locked_at DateTime? @db.DateTime(0)
|
||||||
uuid String? @db.VarChar(36)
|
|
||||||
modified_at DateTime? @db.DateTime(0)
|
modified_at DateTime? @db.DateTime(0)
|
||||||
sync_version Int? @default(0)
|
|
||||||
orders orders[]
|
orders orders[]
|
||||||
projects projects[]
|
projects projects[]
|
||||||
quotation_items quotation_items[]
|
quotation_items quotation_items[]
|
||||||
@@ -492,11 +496,7 @@ model scope_sections {
|
|||||||
title String? @db.VarChar(500)
|
title String? @db.VarChar(500)
|
||||||
title_cz String? @db.VarChar(500)
|
title_cz String? @db.VarChar(500)
|
||||||
content String? @db.Text
|
content String? @db.Text
|
||||||
content_editor_height Int?
|
|
||||||
uuid String? @db.VarChar(36)
|
|
||||||
modified_at DateTime? @db.DateTime(0)
|
modified_at DateTime? @db.DateTime(0)
|
||||||
is_deleted Boolean? @default(false)
|
|
||||||
sync_version Int? @default(0)
|
|
||||||
quotations quotations @relation(fields: [quotation_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "scope_sections_ibfk_1")
|
quotations quotations @relation(fields: [quotation_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "scope_sections_ibfk_1")
|
||||||
|
|
||||||
@@index([quotation_id], map: "quotation_id")
|
@@index([quotation_id], map: "quotation_id")
|
||||||
@@ -590,6 +590,13 @@ model users {
|
|||||||
leave_requests_leave_requests_reviewer_idTousers leave_requests[] @relation("leave_requests_reviewer_idTousers")
|
leave_requests_leave_requests_reviewer_idTousers leave_requests[] @relation("leave_requests_reviewer_idTousers")
|
||||||
projects projects[]
|
projects projects[]
|
||||||
trips trips[]
|
trips trips[]
|
||||||
|
sklad_receipts_received sklad_receipts[] @relation("SkladReceivedBy")
|
||||||
|
sklad_issues_issued sklad_issues[] @relation("SkladIssuedBy")
|
||||||
|
sklad_reservations sklad_reservations[] @relation("SkladReservedBy")
|
||||||
|
work_plan_entries work_plan_entries[] @relation("work_plan_entries_creator")
|
||||||
|
work_plan_entries_owned work_plan_entries[]
|
||||||
|
work_plan_overrides work_plan_overrides[] @relation("work_plan_overrides_creator")
|
||||||
|
work_plan_overrides_owned work_plan_overrides[]
|
||||||
roles roles? @relation(fields: [role_id], references: [id], onUpdate: NoAction, map: "users_ibfk_1")
|
roles roles? @relation(fields: [role_id], references: [id], onUpdate: NoAction, map: "users_ibfk_1")
|
||||||
|
|
||||||
@@index([is_active], map: "idx_users_is_active")
|
@@index([is_active], map: "idx_users_is_active")
|
||||||
@@ -647,3 +654,324 @@ enum attendance_leave_type {
|
|||||||
holiday
|
holiday
|
||||||
unpaid
|
unpaid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum sklad_receipt_status {
|
||||||
|
DRAFT
|
||||||
|
CONFIRMED
|
||||||
|
CANCELLED
|
||||||
|
}
|
||||||
|
|
||||||
|
enum sklad_issue_status {
|
||||||
|
DRAFT
|
||||||
|
CONFIRMED
|
||||||
|
CANCELLED
|
||||||
|
}
|
||||||
|
|
||||||
|
enum sklad_reservation_status {
|
||||||
|
ACTIVE
|
||||||
|
FULFILLED
|
||||||
|
CANCELLED
|
||||||
|
}
|
||||||
|
|
||||||
|
enum sklad_inventory_status {
|
||||||
|
DRAFT
|
||||||
|
CONFIRMED
|
||||||
|
}
|
||||||
|
|
||||||
|
model sklad_categories {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
name String @db.VarChar(100)
|
||||||
|
description String? @db.Text
|
||||||
|
sort_order Int @default(0)
|
||||||
|
created_at DateTime? @default(now()) @db.DateTime(0)
|
||||||
|
modified_at DateTime? @db.DateTime(0)
|
||||||
|
|
||||||
|
items sklad_items[]
|
||||||
|
|
||||||
|
@@map("sklad_categories")
|
||||||
|
}
|
||||||
|
|
||||||
|
model sklad_suppliers {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
name String @db.VarChar(255)
|
||||||
|
ico String? @db.VarChar(20)
|
||||||
|
dic String? @db.VarChar(20)
|
||||||
|
contact_person String? @db.VarChar(255)
|
||||||
|
email String? @db.VarChar(255)
|
||||||
|
phone String? @db.VarChar(50)
|
||||||
|
address String? @db.Text
|
||||||
|
notes String? @db.Text
|
||||||
|
is_active Boolean @default(true)
|
||||||
|
created_at DateTime? @default(now()) @db.DateTime(0)
|
||||||
|
modified_at DateTime? @db.DateTime(0)
|
||||||
|
|
||||||
|
receipts sklad_receipts[]
|
||||||
|
|
||||||
|
@@map("sklad_suppliers")
|
||||||
|
}
|
||||||
|
|
||||||
|
model sklad_locations {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
code String @unique @db.VarChar(20)
|
||||||
|
name String @db.VarChar(100)
|
||||||
|
description String? @db.Text
|
||||||
|
is_active Boolean @default(true)
|
||||||
|
created_at DateTime? @default(now()) @db.DateTime(0)
|
||||||
|
modified_at DateTime? @db.DateTime(0)
|
||||||
|
|
||||||
|
items sklad_item_locations[]
|
||||||
|
receipt_lines sklad_receipt_lines[]
|
||||||
|
issue_lines sklad_issue_lines[]
|
||||||
|
inventory_lines sklad_inventory_lines[]
|
||||||
|
|
||||||
|
@@map("sklad_locations")
|
||||||
|
}
|
||||||
|
|
||||||
|
model sklad_items {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
item_number String? @unique @db.VarChar(50)
|
||||||
|
name String @db.VarChar(255)
|
||||||
|
description String? @db.Text
|
||||||
|
category_id Int?
|
||||||
|
unit String @db.VarChar(20)
|
||||||
|
min_quantity Decimal? @db.Decimal(12, 3)
|
||||||
|
is_active Boolean @default(true)
|
||||||
|
notes String? @db.Text
|
||||||
|
created_at DateTime? @default(now()) @db.DateTime(0)
|
||||||
|
modified_at DateTime? @db.DateTime(0)
|
||||||
|
|
||||||
|
category sklad_categories? @relation(fields: [category_id], references: [id])
|
||||||
|
batches sklad_batches[]
|
||||||
|
item_locations sklad_item_locations[]
|
||||||
|
receipt_lines sklad_receipt_lines[]
|
||||||
|
issue_lines sklad_issue_lines[]
|
||||||
|
reservations sklad_reservations[]
|
||||||
|
inventory_lines sklad_inventory_lines[]
|
||||||
|
|
||||||
|
@@index([category_id], map: "sklad_items_category_id")
|
||||||
|
@@map("sklad_items")
|
||||||
|
}
|
||||||
|
|
||||||
|
model sklad_batches {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
item_id Int
|
||||||
|
receipt_line_id Int @unique
|
||||||
|
quantity Decimal @db.Decimal(12, 3)
|
||||||
|
original_qty Decimal @db.Decimal(12, 3)
|
||||||
|
unit_price Decimal @db.Decimal(12, 2)
|
||||||
|
received_at DateTime? @db.DateTime(0)
|
||||||
|
is_consumed Boolean @default(false)
|
||||||
|
|
||||||
|
item sklad_items @relation(fields: [item_id], references: [id])
|
||||||
|
receipt_line sklad_receipt_lines @relation(fields: [receipt_line_id], references: [id])
|
||||||
|
issue_lines sklad_issue_lines[]
|
||||||
|
|
||||||
|
@@index([item_id, is_consumed, received_at], map: "sklad_batches_fifo")
|
||||||
|
@@map("sklad_batches")
|
||||||
|
}
|
||||||
|
|
||||||
|
model sklad_item_locations {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
item_id Int
|
||||||
|
location_id Int
|
||||||
|
quantity Decimal @default(0) @db.Decimal(12, 3)
|
||||||
|
|
||||||
|
item sklad_items @relation(fields: [item_id], references: [id])
|
||||||
|
location sklad_locations @relation(fields: [location_id], references: [id])
|
||||||
|
|
||||||
|
@@unique([item_id, location_id], map: "sklad_item_locations_unique")
|
||||||
|
@@map("sklad_item_locations")
|
||||||
|
}
|
||||||
|
|
||||||
|
model sklad_receipts {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
receipt_number String? @db.VarChar(50)
|
||||||
|
supplier_id Int?
|
||||||
|
delivery_note_number String? @db.VarChar(100)
|
||||||
|
delivery_note_date DateTime? @db.DateTime(0)
|
||||||
|
received_by Int?
|
||||||
|
notes String? @db.Text
|
||||||
|
status sklad_receipt_status @default(DRAFT)
|
||||||
|
created_at DateTime? @default(now()) @db.DateTime(0)
|
||||||
|
modified_at DateTime? @db.DateTime(0)
|
||||||
|
|
||||||
|
supplier sklad_suppliers? @relation(fields: [supplier_id], references: [id])
|
||||||
|
received_by_user users? @relation("SkladReceivedBy", fields: [received_by], references: [id])
|
||||||
|
items sklad_receipt_lines[]
|
||||||
|
attachments sklad_receipt_attachments[]
|
||||||
|
|
||||||
|
@@map("sklad_receipts")
|
||||||
|
}
|
||||||
|
|
||||||
|
model sklad_receipt_lines {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
receipt_id Int
|
||||||
|
item_id Int
|
||||||
|
quantity Decimal @db.Decimal(12, 3)
|
||||||
|
unit_price Decimal @db.Decimal(12, 2)
|
||||||
|
location_id Int?
|
||||||
|
notes String? @db.VarChar(255)
|
||||||
|
|
||||||
|
receipt sklad_receipts @relation(fields: [receipt_id], references: [id])
|
||||||
|
item sklad_items @relation(fields: [item_id], references: [id])
|
||||||
|
location sklad_locations? @relation(fields: [location_id], references: [id])
|
||||||
|
batch sklad_batches?
|
||||||
|
|
||||||
|
@@index([receipt_id], map: "sklad_receipt_lines_receipt_id")
|
||||||
|
@@map("sklad_receipt_lines")
|
||||||
|
}
|
||||||
|
|
||||||
|
model sklad_receipt_attachments {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
receipt_id Int
|
||||||
|
file_name String @db.VarChar(255)
|
||||||
|
file_mime String @db.VarChar(100)
|
||||||
|
file_size Int
|
||||||
|
file_path String @db.VarChar(500)
|
||||||
|
created_at DateTime? @default(now()) @db.DateTime(0)
|
||||||
|
|
||||||
|
receipt sklad_receipts @relation(fields: [receipt_id], references: [id])
|
||||||
|
|
||||||
|
@@map("sklad_receipt_attachments")
|
||||||
|
}
|
||||||
|
|
||||||
|
model sklad_issues {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
issue_number String? @db.VarChar(50)
|
||||||
|
project_id Int
|
||||||
|
issued_by Int?
|
||||||
|
notes String? @db.Text
|
||||||
|
status sklad_issue_status @default(DRAFT)
|
||||||
|
created_at DateTime? @default(now()) @db.DateTime(0)
|
||||||
|
modified_at DateTime? @db.DateTime(0)
|
||||||
|
|
||||||
|
project projects @relation(fields: [project_id], references: [id])
|
||||||
|
issued_by_user users? @relation("SkladIssuedBy", fields: [issued_by], references: [id])
|
||||||
|
items sklad_issue_lines[]
|
||||||
|
|
||||||
|
@@map("sklad_issues")
|
||||||
|
}
|
||||||
|
|
||||||
|
model sklad_issue_lines {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
issue_id Int
|
||||||
|
item_id Int
|
||||||
|
batch_id Int
|
||||||
|
quantity Decimal @db.Decimal(12, 3)
|
||||||
|
location_id Int?
|
||||||
|
reservation_id Int?
|
||||||
|
notes String? @db.VarChar(255)
|
||||||
|
|
||||||
|
issue sklad_issues @relation(fields: [issue_id], references: [id])
|
||||||
|
item sklad_items @relation(fields: [item_id], references: [id])
|
||||||
|
batch sklad_batches @relation(fields: [batch_id], references: [id])
|
||||||
|
location sklad_locations? @relation(fields: [location_id], references: [id])
|
||||||
|
reservation sklad_reservations? @relation(fields: [reservation_id], references: [id])
|
||||||
|
|
||||||
|
@@index([issue_id], map: "sklad_issue_lines_issue_id")
|
||||||
|
@@map("sklad_issue_lines")
|
||||||
|
}
|
||||||
|
|
||||||
|
model sklad_reservations {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
item_id Int
|
||||||
|
project_id Int
|
||||||
|
quantity Decimal @db.Decimal(12, 3)
|
||||||
|
remaining_qty Decimal @db.Decimal(12, 3)
|
||||||
|
reserved_by Int?
|
||||||
|
notes String? @db.Text
|
||||||
|
status sklad_reservation_status @default(ACTIVE)
|
||||||
|
created_at DateTime? @default(now()) @db.DateTime(0)
|
||||||
|
modified_at DateTime? @db.DateTime(0)
|
||||||
|
|
||||||
|
item sklad_items @relation(fields: [item_id], references: [id])
|
||||||
|
project projects @relation(fields: [project_id], references: [id])
|
||||||
|
reserved_by_user users? @relation("SkladReservedBy", fields: [reserved_by], references: [id])
|
||||||
|
issue_lines sklad_issue_lines[]
|
||||||
|
|
||||||
|
@@index([item_id, status], map: "sklad_reservations_item_status")
|
||||||
|
@@map("sklad_reservations")
|
||||||
|
}
|
||||||
|
|
||||||
|
model sklad_inventory_sessions {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
session_number String? @db.VarChar(50)
|
||||||
|
notes String? @db.Text
|
||||||
|
status sklad_inventory_status @default(DRAFT)
|
||||||
|
created_at DateTime? @default(now()) @db.DateTime(0)
|
||||||
|
modified_at DateTime? @db.DateTime(0)
|
||||||
|
|
||||||
|
items sklad_inventory_lines[]
|
||||||
|
|
||||||
|
@@map("sklad_inventory_sessions")
|
||||||
|
}
|
||||||
|
|
||||||
|
model sklad_inventory_lines {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
session_id Int
|
||||||
|
item_id Int
|
||||||
|
location_id Int?
|
||||||
|
system_qty Decimal @db.Decimal(12, 3)
|
||||||
|
actual_qty Decimal @db.Decimal(12, 3)
|
||||||
|
difference Decimal @db.Decimal(12, 3)
|
||||||
|
notes String? @db.VarChar(255)
|
||||||
|
|
||||||
|
session sklad_inventory_sessions @relation(fields: [session_id], references: [id])
|
||||||
|
item sklad_items @relation(fields: [item_id], references: [id])
|
||||||
|
location sklad_locations? @relation(fields: [location_id], references: [id])
|
||||||
|
|
||||||
|
@@index([session_id], map: "sklad_inventory_lines_session_id")
|
||||||
|
@@map("sklad_inventory_lines")
|
||||||
|
}
|
||||||
|
|
||||||
|
model plan_categories {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
key String @unique @db.VarChar(50)
|
||||||
|
label String @db.VarChar(100)
|
||||||
|
color String @db.VarChar(7)
|
||||||
|
sort_order Int @default(0)
|
||||||
|
is_active Boolean @default(true)
|
||||||
|
created_at DateTime? @default(now()) @db.DateTime(0)
|
||||||
|
updated_at DateTime? @default(now()) @updatedAt @db.DateTime(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
model work_plan_entries {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
user_id Int
|
||||||
|
date_from DateTime @db.Date
|
||||||
|
date_to DateTime @db.Date
|
||||||
|
project_id Int?
|
||||||
|
category String @default("work") @db.VarChar(50)
|
||||||
|
note String @db.VarChar(500)
|
||||||
|
created_by Int
|
||||||
|
created_at DateTime? @default(now()) @db.Timestamp(0)
|
||||||
|
updated_at DateTime? @default(now()) @db.Timestamp(0)
|
||||||
|
is_deleted Boolean? @default(false)
|
||||||
|
users users @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||||
|
projects projects? @relation(fields: [project_id], references: [id], onDelete: SetNull, onUpdate: NoAction)
|
||||||
|
creator users @relation("work_plan_entries_creator", fields: [created_by], references: [id], onDelete: Restrict, onUpdate: NoAction)
|
||||||
|
|
||||||
|
@@index([user_id, date_from], map: "idx_wpe_user_from")
|
||||||
|
@@index([user_id, date_to], map: "idx_wpe_user_to")
|
||||||
|
@@index([project_id], map: "idx_wpe_project")
|
||||||
|
@@index([date_from, date_to], map: "idx_wpe_dates")
|
||||||
|
}
|
||||||
|
|
||||||
|
model work_plan_overrides {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
user_id Int
|
||||||
|
shift_date DateTime @db.Date
|
||||||
|
project_id Int?
|
||||||
|
category String @db.VarChar(50)
|
||||||
|
note String @db.VarChar(500)
|
||||||
|
created_by Int
|
||||||
|
created_at DateTime? @default(now()) @db.Timestamp(0)
|
||||||
|
updated_at DateTime? @default(now()) @db.Timestamp(0)
|
||||||
|
is_deleted Boolean? @default(false)
|
||||||
|
users users @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||||
|
projects projects? @relation(fields: [project_id], references: [id], onDelete: SetNull, onUpdate: NoAction)
|
||||||
|
creator users @relation("work_plan_overrides_creator", fields: [created_by], references: [id], onDelete: Restrict, onUpdate: NoAction)
|
||||||
|
|
||||||
|
@@index([user_id, shift_date], map: "idx_wpo_user_date")
|
||||||
|
@@index([shift_date], map: "idx_wpo_date")
|
||||||
|
}
|
||||||
|
|||||||
@@ -248,7 +248,13 @@ const PERMISSIONS: {
|
|||||||
name: "settings.company",
|
name: "settings.company",
|
||||||
display_name: "Nastavení společnosti",
|
display_name: "Nastavení společnosti",
|
||||||
module: "settings",
|
module: "settings",
|
||||||
description: "Upravovat údaje o společnosti, logo, 2FA",
|
description: "Upravovat údaje o společnosti, logo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "settings.system",
|
||||||
|
display_name: "Systémová nastavení",
|
||||||
|
module: "settings",
|
||||||
|
description: "Spravovat systémová nastavení, 2FA, e-maily, limity",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "settings.banking",
|
name: "settings.banking",
|
||||||
@@ -274,6 +280,32 @@ const PERMISSIONS: {
|
|||||||
module: "settings",
|
module: "settings",
|
||||||
description: "Prohlížet auditní logy",
|
description: "Prohlížet auditní logy",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Sklad
|
||||||
|
{
|
||||||
|
name: "warehouse.view",
|
||||||
|
display_name: "Zobrazit sklad",
|
||||||
|
module: "warehouse",
|
||||||
|
description: "Prohlížet stav skladu, položky, reporty a historii pohybů",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "warehouse.operate",
|
||||||
|
display_name: "Příjem a výdej",
|
||||||
|
module: "warehouse",
|
||||||
|
description: "Vytvářet a potvrzovat příjmy, výdeje a rezervace",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "warehouse.manage",
|
||||||
|
display_name: "Správa skladu",
|
||||||
|
module: "warehouse",
|
||||||
|
description: "Spravovat katalog materiálů, dodavatele, lokace a kategorie",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "warehouse.inventory",
|
||||||
|
display_name: "Inventura",
|
||||||
|
module: "warehouse",
|
||||||
|
description: "Vytvářet a potvrzovat inventurní sčítkání",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
@@ -401,7 +433,14 @@ async function main() {
|
|||||||
|
|
||||||
// 6. Initialize number_sequences for current year
|
// 6. Initialize number_sequences for current year
|
||||||
const year = new Date().getFullYear();
|
const year = new Date().getFullYear();
|
||||||
const types = ["quotation", "order", "invoice"];
|
const types = [
|
||||||
|
"quotation",
|
||||||
|
"order",
|
||||||
|
"invoice",
|
||||||
|
"warehouse_receipt",
|
||||||
|
"warehouse_issue",
|
||||||
|
"warehouse_inventory",
|
||||||
|
];
|
||||||
for (const type of types) {
|
for (const type of types) {
|
||||||
const existing = await prisma.number_sequences.findFirst({
|
const existing = await prisma.number_sequences.findFirst({
|
||||||
where: { type, year },
|
where: { type, year },
|
||||||
|
|||||||
886
src/__tests__/plan.test.ts
Normal file
886
src/__tests__/plan.test.ts
Normal file
@@ -0,0 +1,886 @@
|
|||||||
|
import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
|
||||||
|
import Fastify from "fastify";
|
||||||
|
import cookie from "@fastify/cookie";
|
||||||
|
import rateLimit from "@fastify/rate-limit";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import prisma from "../config/database";
|
||||||
|
import { config } from "../config/env";
|
||||||
|
import { securityHeaders } from "../middleware/security";
|
||||||
|
import planRoutes from "../routes/admin/plan";
|
||||||
|
import {
|
||||||
|
resolveCell,
|
||||||
|
resolveGrid,
|
||||||
|
listPlanUsers,
|
||||||
|
assertNotPastDate,
|
||||||
|
createEntry,
|
||||||
|
updateEntry,
|
||||||
|
deleteEntry,
|
||||||
|
createOverride,
|
||||||
|
updateOverride,
|
||||||
|
deleteOverride,
|
||||||
|
listEntries,
|
||||||
|
listOverrides,
|
||||||
|
} from "../services/plan.service";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Test fixtures
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Note-prefix for test-created rows so we can clean them up safely. */
|
||||||
|
const N = "wh_plan_";
|
||||||
|
|
||||||
|
let adminUserId: number;
|
||||||
|
let noPermUserId: number;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
// Pick the first admin user from the test database as our fixture.
|
||||||
|
// The users -> roles relation is named `roles` in the Prisma schema
|
||||||
|
// (users.role_id references roles.id).
|
||||||
|
const admin = await prisma.users.findFirst({
|
||||||
|
where: { roles: { name: "admin" } },
|
||||||
|
});
|
||||||
|
if (!admin) throw new Error("Test setup: admin user not found");
|
||||||
|
adminUserId = admin.id;
|
||||||
|
|
||||||
|
// For list-scoping tests: any non-admin user that is NOT the admin we use
|
||||||
|
// for create tests. Just pick the second user.
|
||||||
|
const allUsers = await prisma.users.findMany({ take: 2 });
|
||||||
|
noPermUserId =
|
||||||
|
allUsers.find((u) => u.id !== adminUserId)?.id ?? allUsers[0].id;
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Clean up any leftover test data from previous runs. We only touch rows
|
||||||
|
// whose `note` contains our test prefix, so we never disturb real data.
|
||||||
|
await prisma.work_plan_entries.deleteMany({
|
||||||
|
where: { note: { contains: N } },
|
||||||
|
});
|
||||||
|
await prisma.work_plan_overrides.deleteMany({
|
||||||
|
where: { note: { contains: N } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
// Final cleanup so we never leave soft-deleted test rows behind.
|
||||||
|
await prisma.work_plan_entries.deleteMany({
|
||||||
|
where: { note: { contains: N } },
|
||||||
|
});
|
||||||
|
await prisma.work_plan_overrides.deleteMany({
|
||||||
|
where: { note: { contains: N } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// resolveCell
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("plan.service.resolveCell", () => {
|
||||||
|
it("returns null when nothing covers the date", async () => {
|
||||||
|
// No entry, no override for (adminUserId, "2099-01-01")
|
||||||
|
const result = await resolveCell(adminUserId, "2099-01-01");
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the entry that covers the date", async () => {
|
||||||
|
await prisma.work_plan_entries.create({
|
||||||
|
data: {
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: new Date("2099-06-01"),
|
||||||
|
date_to: new Date("2099-06-10"),
|
||||||
|
category: "work",
|
||||||
|
note: `${N}PLC upgrade`,
|
||||||
|
created_by: adminUserId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = await resolveCell(adminUserId, "2099-06-05");
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.source).toBe("entry");
|
||||||
|
expect(result?.note).toBe(`${N}PLC upgrade`);
|
||||||
|
expect(result?.category).toBe("work");
|
||||||
|
expect(result?.rangeFrom).toBe("2099-06-01");
|
||||||
|
expect(result?.rangeTo).toBe("2099-06-10");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the override for that day, not the entry", async () => {
|
||||||
|
// An entry covers 2099-06-01..2099-06-10, but an override on 2099-06-05
|
||||||
|
// takes precedence.
|
||||||
|
await prisma.work_plan_entries.create({
|
||||||
|
data: {
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: new Date("2099-06-01"),
|
||||||
|
date_to: new Date("2099-06-10"),
|
||||||
|
category: "work",
|
||||||
|
note: `${N}PLC upgrade`,
|
||||||
|
created_by: adminUserId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prisma.work_plan_overrides.create({
|
||||||
|
data: {
|
||||||
|
user_id: adminUserId,
|
||||||
|
shift_date: new Date("2099-06-05"),
|
||||||
|
category: "leave",
|
||||||
|
note: `${N}Volno po noční`,
|
||||||
|
created_by: adminUserId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = await resolveCell(adminUserId, "2099-06-05");
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.source).toBe("override");
|
||||||
|
expect(result?.note).toBe(`${N}Volno po noční`);
|
||||||
|
expect(result?.category).toBe("leave");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores soft-deleted entries and overrides", async () => {
|
||||||
|
await prisma.work_plan_entries.create({
|
||||||
|
data: {
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: new Date("2099-06-01"),
|
||||||
|
date_to: new Date("2099-06-10"),
|
||||||
|
category: "work",
|
||||||
|
note: `${N}deleted entry`,
|
||||||
|
is_deleted: true,
|
||||||
|
created_by: adminUserId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = await resolveCell(adminUserId, "2099-06-05");
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// resolveGrid
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("plan.service.resolveGrid", () => {
|
||||||
|
it("returns a 2D map keyed by userId then date", async () => {
|
||||||
|
await prisma.work_plan_entries.create({
|
||||||
|
data: {
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: new Date("2099-06-01"),
|
||||||
|
date_to: new Date("2099-06-03"),
|
||||||
|
category: "work",
|
||||||
|
note: `${N}A`,
|
||||||
|
created_by: adminUserId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const cells = await resolveGrid([adminUserId], "2099-06-01", "2099-06-05");
|
||||||
|
expect(cells[adminUserId]["2099-06-01"]?.note).toBe(`${N}A`);
|
||||||
|
expect(cells[adminUserId]["2099-06-02"]?.note).toBe(`${N}A`);
|
||||||
|
expect(cells[adminUserId]["2099-06-03"]?.note).toBe(`${N}A`);
|
||||||
|
expect(cells[adminUserId]["2099-06-04"]).toBeNull();
|
||||||
|
expect(cells[adminUserId]["2099-06-05"]).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies override on a covered day", async () => {
|
||||||
|
await prisma.work_plan_entries.create({
|
||||||
|
data: {
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: new Date("2099-06-01"),
|
||||||
|
date_to: new Date("2099-06-03"),
|
||||||
|
category: "work",
|
||||||
|
note: `${N}A`,
|
||||||
|
created_by: adminUserId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prisma.work_plan_overrides.create({
|
||||||
|
data: {
|
||||||
|
user_id: adminUserId,
|
||||||
|
shift_date: new Date("2099-06-02"),
|
||||||
|
category: "leave",
|
||||||
|
note: `${N}B`,
|
||||||
|
created_by: adminUserId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const cells = await resolveGrid([adminUserId], "2099-06-01", "2099-06-03");
|
||||||
|
expect(cells[adminUserId]["2099-06-01"]?.note).toBe(`${N}A`);
|
||||||
|
expect(cells[adminUserId]["2099-06-02"]?.note).toBe(`${N}B`);
|
||||||
|
expect(cells[adminUserId]["2099-06-03"]?.note).toBe(`${N}A`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// listPlanUsers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("plan.service.listPlanUsers", () => {
|
||||||
|
it("returns at least the admin user", async () => {
|
||||||
|
const users = await listPlanUsers();
|
||||||
|
const found = users.find((u) => u.id === adminUserId);
|
||||||
|
expect(found).toBeDefined();
|
||||||
|
expect(found?.has_attendance_record).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// assertNotPastDate
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("plan.service.assertNotPastDate", () => {
|
||||||
|
it("returns error for a past date", () => {
|
||||||
|
const result = assertNotPastDate("2000-01-01", false);
|
||||||
|
expect(result).toEqual({
|
||||||
|
error:
|
||||||
|
"Nelze upravovat plán pro datum v minulosti. Pro nouzovou opravu použijte ?force=1.",
|
||||||
|
status: 403,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null for a future date", () => {
|
||||||
|
const result = assertNotPastDate("2099-01-01", false);
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null for a past date with force=true", () => {
|
||||||
|
const result = assertNotPastDate("2000-01-01", true);
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// createEntry
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("plan.service.createEntry", () => {
|
||||||
|
it("creates an entry and returns { data, oldData: null }", async () => {
|
||||||
|
const result = await createEntry(
|
||||||
|
{
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: "2099-07-01",
|
||||||
|
date_to: "2099-07-05",
|
||||||
|
category: "work",
|
||||||
|
note: `${N}new entry`,
|
||||||
|
},
|
||||||
|
adminUserId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect("data" in result).toBe(true);
|
||||||
|
if ("data" in result) {
|
||||||
|
expect(result.data.note).toBe(`${N}new entry`);
|
||||||
|
expect(result.oldData).toBeNull();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a past date without force", async () => {
|
||||||
|
const result = await createEntry(
|
||||||
|
{
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: "2000-01-01",
|
||||||
|
date_to: "2000-01-02",
|
||||||
|
category: "work",
|
||||||
|
note: `${N}past`,
|
||||||
|
},
|
||||||
|
adminUserId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect("error" in result).toBe(true);
|
||||||
|
if ("error" in result) expect(result.status).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows a past date with force", async () => {
|
||||||
|
const result = await createEntry(
|
||||||
|
{
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: "2000-01-01",
|
||||||
|
date_to: "2000-01-02",
|
||||||
|
category: "work",
|
||||||
|
note: `${N}past forced`,
|
||||||
|
},
|
||||||
|
adminUserId,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect("data" in result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects when date_to < date_from", async () => {
|
||||||
|
const result = await createEntry(
|
||||||
|
{
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: "2099-07-10",
|
||||||
|
date_to: "2099-07-05",
|
||||||
|
category: "work",
|
||||||
|
note: `${N}invalid range`,
|
||||||
|
},
|
||||||
|
adminUserId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect("error" in result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows an empty note (text is optional)", async () => {
|
||||||
|
const result = await createEntry(
|
||||||
|
{
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: "2099-07-15",
|
||||||
|
date_to: "2099-07-15",
|
||||||
|
category: "work",
|
||||||
|
note: "",
|
||||||
|
},
|
||||||
|
adminUserId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect("data" in result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a range whose date_to is in the past, even if date_from is today", async () => {
|
||||||
|
// The range check `date_to >= date_from` fires before the past-date
|
||||||
|
// check, so a backwards range returns 400 (correct UX). The to-endpoint
|
||||||
|
// past-date check matters when updating an existing entry — see the
|
||||||
|
// matching test in updateEntry below. Here we just confirm that a
|
||||||
|
// range whose start is in the past is rejected as 403.
|
||||||
|
const result = await createEntry(
|
||||||
|
{
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: "2000-01-01",
|
||||||
|
date_to: "2000-01-05",
|
||||||
|
category: "work",
|
||||||
|
note: `${N}past-both`,
|
||||||
|
},
|
||||||
|
adminUserId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect("error" in result).toBe(true);
|
||||||
|
if ("error" in result) expect(result.status).toBe(403);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// updateEntry
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("plan.service.updateEntry", () => {
|
||||||
|
it("updates the note and returns the old data", async () => {
|
||||||
|
const created = await createEntry(
|
||||||
|
{
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: "2099-08-01",
|
||||||
|
date_to: "2099-08-03",
|
||||||
|
category: "work",
|
||||||
|
note: `${N}original`,
|
||||||
|
},
|
||||||
|
adminUserId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
if (!("data" in created)) throw new Error("setup failed");
|
||||||
|
|
||||||
|
const updated = await updateEntry(
|
||||||
|
created.data.id,
|
||||||
|
{ note: `${N}updated` },
|
||||||
|
adminUserId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect("data" in updated).toBe(true);
|
||||||
|
if ("data" in updated) {
|
||||||
|
expect(updated.data.note).toBe(`${N}updated`);
|
||||||
|
expect((updated.oldData as any).note).toBe(`${N}original`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects an update that pushes date_to into the past", async () => {
|
||||||
|
// Future entry, but the user tries to extend its end backwards into
|
||||||
|
// the past. The to-endpoint past-date check should fire.
|
||||||
|
const created = await createEntry(
|
||||||
|
{
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: "2099-08-10",
|
||||||
|
date_to: "2099-08-15",
|
||||||
|
category: "work",
|
||||||
|
note: `${N}to-shorten`,
|
||||||
|
},
|
||||||
|
adminUserId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
if (!("data" in created)) throw new Error("setup failed");
|
||||||
|
const updated = await updateEntry(
|
||||||
|
created.data.id,
|
||||||
|
{ date_to: "2000-01-05" },
|
||||||
|
adminUserId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect("error" in updated).toBe(true);
|
||||||
|
if ("error" in updated) expect(updated.status).toBe(403);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// deleteEntry
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("plan.service.deleteEntry", () => {
|
||||||
|
it("soft-deletes the row and returns the old data", async () => {
|
||||||
|
const created = await createEntry(
|
||||||
|
{
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: "2099-09-01",
|
||||||
|
date_to: "2099-09-01",
|
||||||
|
category: "work",
|
||||||
|
note: `${N}to delete`,
|
||||||
|
},
|
||||||
|
adminUserId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
if (!("data" in created)) throw new Error("setup failed");
|
||||||
|
|
||||||
|
const result = await deleteEntry(created.data.id, adminUserId, false);
|
||||||
|
expect("data" in result).toBe(true);
|
||||||
|
if ("data" in result) {
|
||||||
|
expect(result.data).toEqual({ ok: true });
|
||||||
|
expect((result.oldData as any).note).toBe(`${N}to delete`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stillThere = await prisma.work_plan_entries.findUnique({
|
||||||
|
where: { id: created.data.id },
|
||||||
|
});
|
||||||
|
expect(stillThere?.is_deleted).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// createOverride
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("plan.service.createOverride", () => {
|
||||||
|
it("creates an override and returns { data, oldData: null, replacedData: null }", async () => {
|
||||||
|
const result = await createOverride(
|
||||||
|
{
|
||||||
|
user_id: adminUserId,
|
||||||
|
shift_date: "2099-10-01",
|
||||||
|
category: "leave",
|
||||||
|
note: `${N}day off`,
|
||||||
|
},
|
||||||
|
adminUserId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect("data" in result).toBe(true);
|
||||||
|
if ("data" in result) {
|
||||||
|
expect(result.data.note).toBe(`${N}day off`);
|
||||||
|
expect(result.oldData).toBeNull();
|
||||||
|
expect(result.replacedData).toBeNull();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("soft-deletes the existing override and reports it in replacedData", async () => {
|
||||||
|
await createOverride(
|
||||||
|
{
|
||||||
|
user_id: adminUserId,
|
||||||
|
shift_date: "2099-10-02",
|
||||||
|
category: "leave",
|
||||||
|
note: `${N}first`,
|
||||||
|
},
|
||||||
|
adminUserId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const second = await createOverride(
|
||||||
|
{
|
||||||
|
user_id: adminUserId,
|
||||||
|
shift_date: "2099-10-02",
|
||||||
|
category: "sick",
|
||||||
|
note: `${N}second`,
|
||||||
|
},
|
||||||
|
adminUserId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect("data" in second).toBe(true);
|
||||||
|
if ("data" in second) {
|
||||||
|
expect((second.replacedData as any)?.note).toBe(`${N}first`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const all = await prisma.work_plan_overrides.findMany({
|
||||||
|
where: { user_id: adminUserId, shift_date: new Date("2099-10-02") },
|
||||||
|
});
|
||||||
|
// The first should be soft-deleted, the second active
|
||||||
|
expect(all.find((o) => o.note === `${N}first`)?.is_deleted).toBe(true);
|
||||||
|
expect(all.find((o) => o.note === `${N}second`)?.is_deleted).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a past date without force", async () => {
|
||||||
|
const result = await createOverride(
|
||||||
|
{
|
||||||
|
user_id: adminUserId,
|
||||||
|
shift_date: "2000-01-01",
|
||||||
|
category: "leave",
|
||||||
|
note: `${N}past`,
|
||||||
|
},
|
||||||
|
adminUserId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect("error" in result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// updateOverride
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("plan.service.updateOverride", () => {
|
||||||
|
it("updates the note and returns the old data", async () => {
|
||||||
|
const created = await createOverride(
|
||||||
|
{
|
||||||
|
user_id: adminUserId,
|
||||||
|
shift_date: "2099-11-01",
|
||||||
|
category: "leave",
|
||||||
|
note: `${N}original`,
|
||||||
|
},
|
||||||
|
adminUserId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
if (!("data" in created)) throw new Error("setup");
|
||||||
|
const updated = await updateOverride(
|
||||||
|
created.data.id,
|
||||||
|
{ note: `${N}updated` },
|
||||||
|
adminUserId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect("data" in updated).toBe(true);
|
||||||
|
if ("data" in updated) {
|
||||||
|
expect(updated.data.note).toBe(`${N}updated`);
|
||||||
|
expect((updated.oldData as any).note).toBe(`${N}original`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// deleteOverride
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("plan.service.deleteOverride", () => {
|
||||||
|
it("soft-deletes the override and returns the old data", async () => {
|
||||||
|
const created = await createOverride(
|
||||||
|
{
|
||||||
|
user_id: adminUserId,
|
||||||
|
shift_date: "2099-12-01",
|
||||||
|
category: "leave",
|
||||||
|
note: `${N}to delete`,
|
||||||
|
},
|
||||||
|
adminUserId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
if (!("data" in created)) throw new Error("setup");
|
||||||
|
const result = await deleteOverride(created.data.id, adminUserId, false);
|
||||||
|
expect("data" in result).toBe(true);
|
||||||
|
if ("data" in result) {
|
||||||
|
expect(result.data).toEqual({ ok: true });
|
||||||
|
expect((result.oldData as any).note).toBe(`${N}to delete`);
|
||||||
|
}
|
||||||
|
const still = await prisma.work_plan_overrides.findUnique({
|
||||||
|
where: { id: created.data.id },
|
||||||
|
});
|
||||||
|
expect(still?.is_deleted).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// listEntries / listOverrides (employee scoping)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("plan.service.listEntries (employee scoping)", () => {
|
||||||
|
it("returns entries for the actor user when scoped", async () => {
|
||||||
|
await createEntry(
|
||||||
|
{
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: "2098-01-01",
|
||||||
|
date_to: "2098-01-05",
|
||||||
|
category: "work",
|
||||||
|
note: `${N}scope test`,
|
||||||
|
},
|
||||||
|
adminUserId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const rows = await listEntries(
|
||||||
|
{ user_id: adminUserId, date_from: "2098-01-01", date_to: "2098-01-31" },
|
||||||
|
adminUserId,
|
||||||
|
true, // isAdmin
|
||||||
|
);
|
||||||
|
expect(rows.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("scopes non-admin to actor user_id when querying for another user", async () => {
|
||||||
|
await createEntry(
|
||||||
|
{
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: "2098-02-01",
|
||||||
|
date_to: "2098-02-05",
|
||||||
|
category: "work",
|
||||||
|
note: `${N}admin entry`,
|
||||||
|
},
|
||||||
|
adminUserId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const rows = await listEntries(
|
||||||
|
{ user_id: adminUserId, date_from: "2098-02-01", date_to: "2098-02-28" },
|
||||||
|
noPermUserId,
|
||||||
|
false, // not admin
|
||||||
|
);
|
||||||
|
expect(rows).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("plan.service.listOverrides (employee scoping)", () => {
|
||||||
|
it("scopes non-admin to actor user_id", async () => {
|
||||||
|
await createOverride(
|
||||||
|
{
|
||||||
|
user_id: adminUserId,
|
||||||
|
shift_date: "2098-03-01",
|
||||||
|
category: "leave",
|
||||||
|
note: `${N}admin override`,
|
||||||
|
},
|
||||||
|
adminUserId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const rows = await listOverrides(
|
||||||
|
{ user_id: adminUserId, date_from: "2098-03-01", date_to: "2098-03-31" },
|
||||||
|
noPermUserId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect(rows).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// HTTP route tests (Fastify inject)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Build a Fastify app with only the plan routes registered under
|
||||||
|
// /api/admin/plan. rateLimit max=1000 keeps the 7 tests from tripping it.
|
||||||
|
let app: Awaited<ReturnType<typeof buildApp>>;
|
||||||
|
let adminToken: string;
|
||||||
|
let noPermToken: string;
|
||||||
|
let noPermRoleId: number;
|
||||||
|
|
||||||
|
async function buildApp() {
|
||||||
|
const a = Fastify({ logger: false });
|
||||||
|
await a.register(cookie);
|
||||||
|
await a.register(rateLimit, { max: 1000, timeWindow: "1 minute" });
|
||||||
|
a.addHook("onRequest", securityHeaders);
|
||||||
|
await a.register(planRoutes, { prefix: "/api/admin/plan" });
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a JWT access token. `requireAuth` re-loads auth data from the
|
||||||
|
* DB via `loadAuthData(payload.sub)`, so the `role` claim is ignored —
|
||||||
|
* only the `sub` (user id) matters for these tests.
|
||||||
|
*/
|
||||||
|
function generateToken(user: {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
roleName: string | null;
|
||||||
|
}): string {
|
||||||
|
return jwt.sign(
|
||||||
|
{ sub: user.id, username: user.username, role: user.roleName },
|
||||||
|
config.jwt.secret,
|
||||||
|
{ expiresIn: "15m" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function authGet(path: string, token: string) {
|
||||||
|
return app.inject({
|
||||||
|
method: "GET",
|
||||||
|
url: path,
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function authPost(path: string, token: string, body: unknown) {
|
||||||
|
return app.inject({
|
||||||
|
method: "POST",
|
||||||
|
url: path,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
payload: body as object,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function authPatch(path: string, token: string, body: unknown) {
|
||||||
|
return app.inject({
|
||||||
|
method: "PATCH",
|
||||||
|
url: path,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
payload: body as object,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function authDelete(path: string, token: string) {
|
||||||
|
return app.inject({
|
||||||
|
method: "DELETE",
|
||||||
|
url: path,
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await buildApp();
|
||||||
|
|
||||||
|
// Re-fetch the admin user with the roles relation so we can build a token.
|
||||||
|
const admin = await prisma.users.findFirst({
|
||||||
|
where: { roles: { name: "admin" } },
|
||||||
|
include: { roles: true },
|
||||||
|
});
|
||||||
|
if (!admin) throw new Error("admin user not found in test setup");
|
||||||
|
adminUserId = admin.id;
|
||||||
|
adminToken = generateToken({
|
||||||
|
id: admin.id,
|
||||||
|
username: admin.username,
|
||||||
|
roleName: admin.roles?.name ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a no-permission role + user; the role has zero permissions so
|
||||||
|
// any permission check returns 403.
|
||||||
|
const stamp = Date.now();
|
||||||
|
const noPermRole = await prisma.roles.create({
|
||||||
|
data: {
|
||||||
|
name: `noperm_plan_${stamp}`,
|
||||||
|
display_name: "No Perm Plan Test",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
noPermRoleId = noPermRole.id;
|
||||||
|
const noPermUser = await prisma.users.create({
|
||||||
|
data: {
|
||||||
|
username: `noperm_plan_${stamp}`,
|
||||||
|
first_name: "No",
|
||||||
|
last_name: "Perm",
|
||||||
|
email: `noperm_plan_${stamp}@test.local`,
|
||||||
|
password_hash:
|
||||||
|
"$2a$10$invalidinvalidinvalidinvalidinvalidinvalidinvalidinvali",
|
||||||
|
role_id: noPermRole.id,
|
||||||
|
is_active: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
noPermUserId = noPermUser.id;
|
||||||
|
noPermToken = generateToken({
|
||||||
|
id: noPermUser.id,
|
||||||
|
username: noPermUser.username,
|
||||||
|
roleName: noPermRole.name,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (app) await app.close();
|
||||||
|
if (noPermUserId) {
|
||||||
|
await prisma.users
|
||||||
|
.deleteMany({ where: { id: noPermUserId } })
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
if (noPermRoleId) {
|
||||||
|
await prisma.roles
|
||||||
|
.deleteMany({ where: { id: noPermRoleId } })
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("HTTP /api/admin/plan", () => {
|
||||||
|
it("GET /users requires auth", async () => {
|
||||||
|
const res = await app.inject({
|
||||||
|
method: "GET",
|
||||||
|
url: "/api/admin/plan/users",
|
||||||
|
});
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("GET /users returns plan users for an admin", async () => {
|
||||||
|
const res = await authGet("/api/admin/plan/users", adminToken);
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
const body = res.json();
|
||||||
|
expect(body.success).toBe(true);
|
||||||
|
expect(Array.isArray(body.data)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("GET /grid returns days, users, cells", async () => {
|
||||||
|
// Seed an entry so the cell on 2097-06-01 has a known note.
|
||||||
|
await createEntry(
|
||||||
|
{
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: "2097-06-01",
|
||||||
|
date_to: "2097-06-05",
|
||||||
|
category: "work",
|
||||||
|
note: `${N}grid test`,
|
||||||
|
},
|
||||||
|
adminUserId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const res = await authGet(
|
||||||
|
"/api/admin/plan/grid?date_from=2097-06-01&date_to=2097-06-05",
|
||||||
|
adminToken,
|
||||||
|
);
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
const body = res.json();
|
||||||
|
expect(body.data.users).toBeDefined();
|
||||||
|
expect(body.data.cells[adminUserId]).toBeDefined();
|
||||||
|
expect(body.data.cells[adminUserId]["2097-06-01"].note).toBe(
|
||||||
|
`${N}grid test`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("GET /grid rejects ranges > 92 days", async () => {
|
||||||
|
const res = await authGet(
|
||||||
|
"/api/admin/plan/grid?date_from=2097-01-01&date_to=2097-12-31",
|
||||||
|
adminToken,
|
||||||
|
);
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("POST /entries requires attendance.manage", async () => {
|
||||||
|
const res = await authPost("/api/admin/plan/entries", noPermToken, {
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: "2097-07-01",
|
||||||
|
date_to: "2097-07-01",
|
||||||
|
category: "work",
|
||||||
|
note: `${N}forbidden`,
|
||||||
|
});
|
||||||
|
expect(res.statusCode).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("POST /entries works for admin", async () => {
|
||||||
|
const res = await authPost("/api/admin/plan/entries", adminToken, {
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: "2097-08-01",
|
||||||
|
date_to: "2097-08-01",
|
||||||
|
category: "work",
|
||||||
|
note: `${N}http create`,
|
||||||
|
});
|
||||||
|
expect(res.statusCode).toBe(201);
|
||||||
|
const body = res.json();
|
||||||
|
expect(body.success).toBe(true);
|
||||||
|
expect(body.data.note).toBe(`${N}http create`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("POST /entries accepts an empty note (note is optional)", async () => {
|
||||||
|
const res = await authPost("/api/admin/plan/entries", adminToken, {
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: "2097-08-05",
|
||||||
|
date_to: "2097-08-05",
|
||||||
|
category: "work",
|
||||||
|
note: "",
|
||||||
|
});
|
||||||
|
expect(res.statusCode).toBe(201);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("POST /entries rejects a past date without ?force=1", async () => {
|
||||||
|
const res = await authPost("/api/admin/plan/entries", adminToken, {
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: "2000-02-01",
|
||||||
|
date_to: "2000-02-01",
|
||||||
|
category: "work",
|
||||||
|
note: `${N}past http`,
|
||||||
|
});
|
||||||
|
expect(res.statusCode).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("POST /overrides with force=1 allows past date", async () => {
|
||||||
|
const res = await authPost(
|
||||||
|
"/api/admin/plan/overrides?force=1",
|
||||||
|
adminToken,
|
||||||
|
{
|
||||||
|
user_id: adminUserId,
|
||||||
|
shift_date: "2000-01-15",
|
||||||
|
category: "leave",
|
||||||
|
note: `${N}forced past`,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(res.statusCode).toBe(201);
|
||||||
|
});
|
||||||
|
});
|
||||||
95
src/__tests__/planAuditDescription.test.ts
Normal file
95
src/__tests__/planAuditDescription.test.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import {
|
||||||
|
planCategoryLabel,
|
||||||
|
buildPlanAuditDescription,
|
||||||
|
} from "../utils/planAuditDescription";
|
||||||
|
|
||||||
|
describe("planCategoryLabel", () => {
|
||||||
|
it("maps known plan categories to Czech labels", () => {
|
||||||
|
expect(planCategoryLabel("work")).toBe("Práce");
|
||||||
|
expect(planCategoryLabel("preparation")).toBe("Příprava");
|
||||||
|
expect(planCategoryLabel("travel")).toBe("Cesta / Montáž");
|
||||||
|
expect(planCategoryLabel("leave")).toBe("Dovolená");
|
||||||
|
expect(planCategoryLabel("sick")).toBe("Nemoc");
|
||||||
|
expect(planCategoryLabel("training")).toBe("Školení");
|
||||||
|
expect(planCategoryLabel("other")).toBe("Jiné");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to the raw value for an unknown category", () => {
|
||||||
|
expect(planCategoryLabel("unknown_xyz")).toBe("unknown_xyz");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("buildPlanAuditDescription", () => {
|
||||||
|
it("describes a multi-day entry with project", () => {
|
||||||
|
const desc = buildPlanAuditDescription({
|
||||||
|
userName: "Jan Novák",
|
||||||
|
categoryLabel: "Práce",
|
||||||
|
projectName: "Rekonstrukce haly",
|
||||||
|
dateFrom: "2026-06-08",
|
||||||
|
dateTo: "2026-06-12",
|
||||||
|
});
|
||||||
|
expect(desc).toBe(
|
||||||
|
"Jan Novák · 08.06.2026 – 12.06.2026 · Práce · Rekonstrukce haly",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("collapses a single-day range to one date", () => {
|
||||||
|
const desc = buildPlanAuditDescription({
|
||||||
|
userName: "Jan Novák",
|
||||||
|
categoryLabel: "Práce",
|
||||||
|
projectName: "Rekonstrukce haly",
|
||||||
|
dateFrom: "2026-06-08",
|
||||||
|
dateTo: "2026-06-08",
|
||||||
|
});
|
||||||
|
expect(desc).toBe("Jan Novák · 08.06.2026 · Práce · Rekonstrukce haly");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits the project segment when there is no project", () => {
|
||||||
|
const desc = buildPlanAuditDescription({
|
||||||
|
userName: "Petr Svoboda",
|
||||||
|
categoryLabel: "Dovolená",
|
||||||
|
projectName: null,
|
||||||
|
dateFrom: "2026-06-10",
|
||||||
|
dateTo: "2026-06-10",
|
||||||
|
});
|
||||||
|
expect(desc).toBe("Petr Svoboda · 10.06.2026 · Dovolená");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("appends the emergency-edit note when force is set", () => {
|
||||||
|
const desc = buildPlanAuditDescription({
|
||||||
|
userName: "Jan Novák",
|
||||||
|
categoryLabel: "Práce",
|
||||||
|
projectName: null,
|
||||||
|
dateFrom: "2026-01-02",
|
||||||
|
dateTo: "2026-01-02",
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
expect(desc).toBe("Jan Novák · 02.01.2026 · Práce · nouzová úprava");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("appends a custom suffix (e.g. replaced override)", () => {
|
||||||
|
const desc = buildPlanAuditDescription({
|
||||||
|
userName: "Jan Novák",
|
||||||
|
categoryLabel: "Nemoc",
|
||||||
|
projectName: null,
|
||||||
|
dateFrom: "2026-06-10",
|
||||||
|
dateTo: "2026-06-10",
|
||||||
|
suffix: "nahrazeno novým záznamem",
|
||||||
|
});
|
||||||
|
expect(desc).toBe(
|
||||||
|
"Jan Novák · 10.06.2026 · Nemoc · nahrazeno novým záznamem",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats a whitespace-only project name as no project", () => {
|
||||||
|
const desc = buildPlanAuditDescription({
|
||||||
|
userName: "Jan Novák",
|
||||||
|
categoryLabel: "Práce",
|
||||||
|
projectName: " ",
|
||||||
|
dateFrom: "2026-06-08",
|
||||||
|
dateTo: "2026-06-08",
|
||||||
|
});
|
||||||
|
expect(desc).toBe("Jan Novák · 08.06.2026 · Práce");
|
||||||
|
});
|
||||||
|
});
|
||||||
129
src/__tests__/planCategory.test.ts
Normal file
129
src/__tests__/planCategory.test.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import { describe, it, expect, afterAll } from "vitest";
|
||||||
|
import prisma from "../config/database";
|
||||||
|
import {
|
||||||
|
listPlanCategories,
|
||||||
|
createPlanCategory,
|
||||||
|
updatePlanCategory,
|
||||||
|
deactivatePlanCategory,
|
||||||
|
deletePlanCategory,
|
||||||
|
isCategoryKeyActive,
|
||||||
|
slugifyCategory,
|
||||||
|
} from "../services/planCategory.service";
|
||||||
|
|
||||||
|
const created: number[] = [];
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (created.length) {
|
||||||
|
await prisma.plan_categories.deleteMany({ where: { id: { in: created } } });
|
||||||
|
}
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("slugifyCategory", () => {
|
||||||
|
it("strips Czech diacritics and non-alphanumerics", () => {
|
||||||
|
expect(slugifyCategory("Cesta / Montáž")).toBe("cesta_montaz");
|
||||||
|
expect(slugifyCategory("Příprava")).toBe("priprava");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("plan category service", () => {
|
||||||
|
it("seeds are present and ordered", async () => {
|
||||||
|
const cats = await listPlanCategories(true);
|
||||||
|
const keys = cats.map((c) => c.key);
|
||||||
|
expect(keys).toContain("work");
|
||||||
|
expect(keys).toContain("other");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates a category with a unique slug and next sort_order", async () => {
|
||||||
|
const res = await createPlanCategory({
|
||||||
|
label: "Test Kategorie ZZ",
|
||||||
|
color: "#123456",
|
||||||
|
});
|
||||||
|
if ("error" in res) throw new Error(res.error);
|
||||||
|
created.push(res.data.id);
|
||||||
|
expect(res.data.key).toBe("test_kategorie_zz");
|
||||||
|
expect(res.data.color).toBe("#123456");
|
||||||
|
expect(res.data.is_active).toBe(true);
|
||||||
|
expect(await isCategoryKeyActive("test_kategorie_zz")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dedupes a colliding slug", async () => {
|
||||||
|
const res = await createPlanCategory({
|
||||||
|
label: "Test Kategorie ZZ",
|
||||||
|
color: "#222222",
|
||||||
|
});
|
||||||
|
if ("error" in res) throw new Error(res.error);
|
||||||
|
created.push(res.data.id);
|
||||||
|
expect(res.data.key).toBe("test_kategorie_zz_2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates label and color (key immutable)", async () => {
|
||||||
|
const res = await createPlanCategory({
|
||||||
|
label: "Test Upd ZZ",
|
||||||
|
color: "#aaaaaa",
|
||||||
|
});
|
||||||
|
if ("error" in res) throw new Error(res.error);
|
||||||
|
created.push(res.data.id);
|
||||||
|
const upd = await updatePlanCategory(res.data.id, {
|
||||||
|
label: "Test Upd2 ZZ",
|
||||||
|
color: "#bbbbbb",
|
||||||
|
});
|
||||||
|
if ("error" in upd) throw new Error(upd.error);
|
||||||
|
expect(upd.data.label).toBe("Test Upd2 ZZ");
|
||||||
|
expect(upd.data.color).toBe("#bbbbbb");
|
||||||
|
expect(upd.data.key).toBe("test_upd_zz");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deactivate hides the key from active check", async () => {
|
||||||
|
const res = await createPlanCategory({
|
||||||
|
label: "Test Deact ZZ",
|
||||||
|
color: "#cccccc",
|
||||||
|
});
|
||||||
|
if ("error" in res) throw new Error(res.error);
|
||||||
|
created.push(res.data.id);
|
||||||
|
await deactivatePlanCategory(res.data.id);
|
||||||
|
expect(await isCategoryKeyActive("test_deact_zz")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hard-deletes an unused category", async () => {
|
||||||
|
const res = await createPlanCategory({
|
||||||
|
label: "Test Del ZZ",
|
||||||
|
color: "#dddddd",
|
||||||
|
});
|
||||||
|
if ("error" in res) throw new Error(res.error);
|
||||||
|
const del = await deletePlanCategory(res.data.id);
|
||||||
|
expect("data" in del).toBe(true);
|
||||||
|
const stillThere = await prisma.plan_categories.findUnique({
|
||||||
|
where: { id: res.data.id },
|
||||||
|
});
|
||||||
|
expect(stillThere).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("refuses to hard-delete a category in use", async () => {
|
||||||
|
const res = await createPlanCategory({
|
||||||
|
label: "Test InUse ZZ",
|
||||||
|
color: "#eeeeee",
|
||||||
|
});
|
||||||
|
if ("error" in res) throw new Error(res.error);
|
||||||
|
created.push(res.data.id);
|
||||||
|
const user = await prisma.users.findFirst({ select: { id: true } });
|
||||||
|
if (!user) throw new Error("no user in test DB");
|
||||||
|
const entry = await prisma.work_plan_entries.create({
|
||||||
|
data: {
|
||||||
|
user_id: user.id,
|
||||||
|
date_from: new Date("2026-06-10T00:00:00.000Z"),
|
||||||
|
date_to: new Date("2026-06-10T00:00:00.000Z"),
|
||||||
|
category: res.data.key,
|
||||||
|
note: "test in-use",
|
||||||
|
created_by: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const del = await deletePlanCategory(res.data.id);
|
||||||
|
expect("error" in del).toBe(true);
|
||||||
|
if ("error" in del) expect(del.status).toBe(409);
|
||||||
|
} finally {
|
||||||
|
await prisma.work_plan_entries.delete({ where: { id: entry.id } });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
1244
src/__tests__/warehouse.test.ts
Normal file
1244
src/__tests__/warehouse.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -17,16 +17,17 @@ import "./layout.css";
|
|||||||
import "./components.css";
|
import "./components.css";
|
||||||
import "./tables.css";
|
import "./tables.css";
|
||||||
import "./datepicker.css";
|
import "./datepicker.css";
|
||||||
import "./bones/registry";
|
|
||||||
import "./filemanager.css";
|
import "./filemanager.css";
|
||||||
import "./pagination.css";
|
import "./pagination.css";
|
||||||
import "./responsive.css";
|
import "./responsive.css";
|
||||||
import "./login.css";
|
import "./login.css";
|
||||||
import "./dashboard.css";
|
import "./dashboard.css";
|
||||||
import "./attendance.css";
|
import "./attendance.css";
|
||||||
|
import "./plan.css";
|
||||||
import "./settings.css";
|
import "./settings.css";
|
||||||
import "./offers.css";
|
import "./offers.css";
|
||||||
import "./invoices.css";
|
import "./invoices.css";
|
||||||
|
import "./warehouse.css";
|
||||||
|
|
||||||
const Users = lazy(() => import("./pages/Users"));
|
const Users = lazy(() => import("./pages/Users"));
|
||||||
const Attendance = lazy(() => import("./pages/Attendance"));
|
const Attendance = lazy(() => import("./pages/Attendance"));
|
||||||
@@ -37,6 +38,7 @@ const AttendanceCreate = lazy(() => import("./pages/AttendanceCreate"));
|
|||||||
const LeaveRequests = lazy(() => import("./pages/LeaveRequests"));
|
const LeaveRequests = lazy(() => import("./pages/LeaveRequests"));
|
||||||
const LeaveApproval = lazy(() => import("./pages/LeaveApproval"));
|
const LeaveApproval = lazy(() => import("./pages/LeaveApproval"));
|
||||||
const AttendanceLocation = lazy(() => import("./pages/AttendanceLocation"));
|
const AttendanceLocation = lazy(() => import("./pages/AttendanceLocation"));
|
||||||
|
const PlanWork = lazy(() => import("./pages/PlanWork"));
|
||||||
const Trips = lazy(() => import("./pages/Trips"));
|
const Trips = lazy(() => import("./pages/Trips"));
|
||||||
const TripsHistory = lazy(() => import("./pages/TripsHistory"));
|
const TripsHistory = lazy(() => import("./pages/TripsHistory"));
|
||||||
const TripsAdmin = lazy(() => import("./pages/TripsAdmin"));
|
const TripsAdmin = lazy(() => import("./pages/TripsAdmin"));
|
||||||
@@ -54,6 +56,31 @@ const InvoiceDetail = lazy(() => import("./pages/InvoiceDetail"));
|
|||||||
const Settings = lazy(() => import("./pages/Settings"));
|
const Settings = lazy(() => import("./pages/Settings"));
|
||||||
const AuditLog = lazy(() => import("./pages/AuditLog"));
|
const AuditLog = lazy(() => import("./pages/AuditLog"));
|
||||||
const NotFound = lazy(() => import("./pages/NotFound"));
|
const NotFound = lazy(() => import("./pages/NotFound"));
|
||||||
|
const Warehouse = lazy(() => import("./pages/Warehouse"));
|
||||||
|
const WarehouseItems = lazy(() => import("./pages/WarehouseItems"));
|
||||||
|
const WarehouseItemDetail = lazy(() => import("./pages/WarehouseItemDetail"));
|
||||||
|
const WarehouseReceipts = lazy(() => import("./pages/WarehouseReceipts"));
|
||||||
|
const WarehouseReceiptDetail = lazy(
|
||||||
|
() => import("./pages/WarehouseReceiptDetail"),
|
||||||
|
);
|
||||||
|
const WarehouseReceiptForm = lazy(() => import("./pages/WarehouseReceiptForm"));
|
||||||
|
const WarehouseIssues = lazy(() => import("./pages/WarehouseIssues"));
|
||||||
|
const WarehouseIssueDetail = lazy(() => import("./pages/WarehouseIssueDetail"));
|
||||||
|
const WarehouseIssueForm = lazy(() => import("./pages/WarehouseIssueForm"));
|
||||||
|
const WarehouseReservations = lazy(
|
||||||
|
() => import("./pages/WarehouseReservations"),
|
||||||
|
);
|
||||||
|
const WarehouseInventory = lazy(() => import("./pages/WarehouseInventory"));
|
||||||
|
const WarehouseInventoryDetail = lazy(
|
||||||
|
() => import("./pages/WarehouseInventoryDetail"),
|
||||||
|
);
|
||||||
|
const WarehouseInventoryForm = lazy(
|
||||||
|
() => import("./pages/WarehouseInventoryForm"),
|
||||||
|
);
|
||||||
|
const WarehouseReports = lazy(() => import("./pages/WarehouseReports"));
|
||||||
|
const WarehouseSuppliers = lazy(() => import("./pages/WarehouseSuppliers"));
|
||||||
|
const WarehouseLocations = lazy(() => import("./pages/WarehouseLocations"));
|
||||||
|
const WarehouseCategories = lazy(() => import("./pages/WarehouseCategories"));
|
||||||
|
|
||||||
export default function AdminApp() {
|
export default function AdminApp() {
|
||||||
return (
|
return (
|
||||||
@@ -103,6 +130,7 @@ export default function AdminApp() {
|
|||||||
path="attendance/location/:id"
|
path="attendance/location/:id"
|
||||||
element={<AttendanceLocation />}
|
element={<AttendanceLocation />}
|
||||||
/>
|
/>
|
||||||
|
<Route path="plan-work" element={<PlanWork />} />
|
||||||
<Route path="trips" element={<Trips />} />
|
<Route path="trips" element={<Trips />} />
|
||||||
<Route path="trips/history" element={<TripsHistory />} />
|
<Route path="trips/history" element={<TripsHistory />} />
|
||||||
<Route path="trips/admin" element={<TripsAdmin />} />
|
<Route path="trips/admin" element={<TripsAdmin />} />
|
||||||
@@ -127,6 +155,76 @@ export default function AdminApp() {
|
|||||||
<Route path="invoices/:id" element={<InvoiceDetail />} />
|
<Route path="invoices/:id" element={<InvoiceDetail />} />
|
||||||
<Route path="settings" element={<Settings />} />
|
<Route path="settings" element={<Settings />} />
|
||||||
<Route path="audit-log" element={<AuditLog />} />
|
<Route path="audit-log" element={<AuditLog />} />
|
||||||
|
<Route path="warehouse" element={<Warehouse />} />
|
||||||
|
<Route path="warehouse/items" element={<WarehouseItems />} />
|
||||||
|
<Route
|
||||||
|
path="warehouse/items/:id"
|
||||||
|
element={<WarehouseItemDetail />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="warehouse/receipts"
|
||||||
|
element={<WarehouseReceipts />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="warehouse/receipts/new"
|
||||||
|
element={<WarehouseReceiptForm />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="warehouse/receipts/:id"
|
||||||
|
element={<WarehouseReceiptDetail />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="warehouse/receipts/:id/edit"
|
||||||
|
element={<WarehouseReceiptForm />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="warehouse/issues"
|
||||||
|
element={<WarehouseIssues />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="warehouse/issues/new"
|
||||||
|
element={<WarehouseIssueForm />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="warehouse/issues/:id"
|
||||||
|
element={<WarehouseIssueDetail />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="warehouse/issues/:id/edit"
|
||||||
|
element={<WarehouseIssueForm />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="warehouse/reservations"
|
||||||
|
element={<WarehouseReservations />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="warehouse/inventory"
|
||||||
|
element={<WarehouseInventory />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="warehouse/inventory/new"
|
||||||
|
element={<WarehouseInventoryForm />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="warehouse/inventory/:id"
|
||||||
|
element={<WarehouseInventoryDetail />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="warehouse/reports"
|
||||||
|
element={<WarehouseReports />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="warehouse/suppliers"
|
||||||
|
element={<WarehouseSuppliers />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="warehouse/locations"
|
||||||
|
element={<WarehouseLocations />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="warehouse/categories"
|
||||||
|
element={<WarehouseCategories />}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -318,12 +318,14 @@
|
|||||||
|
|
||||||
/* Leave Type Badges */
|
/* Leave Type Badges */
|
||||||
.attendance-leave-badge {
|
.attendance-leave-badge {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
padding: 0.25rem 0.5rem;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.25rem 0.55rem;
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-right: 0.25rem;
|
line-height: 1.2;
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,263 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "attendance-create",
|
|
||||||
"viewportWidth": 351,
|
|
||||||
"width": 351,
|
|
||||||
"height": 403,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
42,
|
|
||||||
100,
|
|
||||||
361,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
55,
|
|
||||||
92.5926,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
83,
|
|
||||||
92.5926,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
127,
|
|
||||||
92.5926,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
156,
|
|
||||||
92.5926,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
200,
|
|
||||||
92.5926,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
229,
|
|
||||||
92.5926,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
273,
|
|
||||||
92.5926,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
302,
|
|
||||||
92.5926,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
346,
|
|
||||||
19.2352,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "attendance-create",
|
|
||||||
"viewportWidth": 736,
|
|
||||||
"width": 736,
|
|
||||||
"height": 420,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
23.3738,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
46,
|
|
||||||
81.5217,
|
|
||||||
373,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
65,
|
|
||||||
76.3587,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
94,
|
|
||||||
76.3587,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
138,
|
|
||||||
76.3587,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
167,
|
|
||||||
76.3587,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
211,
|
|
||||||
76.3587,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
240,
|
|
||||||
76.3587,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
284,
|
|
||||||
76.3587,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
313,
|
|
||||||
76.3587,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
357,
|
|
||||||
9.1733,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "attendance-create",
|
|
||||||
"viewportWidth": 996,
|
|
||||||
"width": 996,
|
|
||||||
"height": 369,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
17.2722,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
46,
|
|
||||||
60.241,
|
|
||||||
323,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
65,
|
|
||||||
56.4257,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
93,
|
|
||||||
56.4257,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
129,
|
|
||||||
56.4257,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
156,
|
|
||||||
56.4257,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
192,
|
|
||||||
56.4257,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
219,
|
|
||||||
56.4257,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
255,
|
|
||||||
56.4257,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
282,
|
|
||||||
56.4257,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
318,
|
|
||||||
6.3771,
|
|
||||||
32,
|
|
||||||
8
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "7c4e446bf97f164a0fba87e2dd7df7d1"
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,506 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "dash-sessions",
|
|
||||||
"viewportWidth": 341,
|
|
||||||
"width": 341,
|
|
||||||
"height": 304,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
304,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.8123,
|
|
||||||
17,
|
|
||||||
34.9386,
|
|
||||||
17,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
59.3383,
|
|
||||||
13,
|
|
||||||
36.8493,
|
|
||||||
37,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0.2933,
|
|
||||||
63,
|
|
||||||
99.4135,
|
|
||||||
83,
|
|
||||||
8,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.9853,
|
|
||||||
86,
|
|
||||||
10.5572,
|
|
||||||
36,
|
|
||||||
8,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
7.6246,
|
|
||||||
95,
|
|
||||||
5.2786,
|
|
||||||
18,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
63.0407,
|
|
||||||
79,
|
|
||||||
19.5152,
|
|
||||||
27,
|
|
||||||
9999
|
|
||||||
],
|
|
||||||
[
|
|
||||||
19.0616,
|
|
||||||
110,
|
|
||||||
21.6734,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
43.081,
|
|
||||||
110,
|
|
||||||
1.0585,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
46.4855,
|
|
||||||
110,
|
|
||||||
26.8649,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.9853,
|
|
||||||
167,
|
|
||||||
10.5572,
|
|
||||||
36,
|
|
||||||
8,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
7.6246,
|
|
||||||
176,
|
|
||||||
5.2786,
|
|
||||||
18,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
19.0616,
|
|
||||||
162,
|
|
||||||
75.9531,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
19.0616,
|
|
||||||
189,
|
|
||||||
21.6734,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
43.081,
|
|
||||||
189,
|
|
||||||
1.0585,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
46.4855,
|
|
||||||
189,
|
|
||||||
26.8649,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.9853,
|
|
||||||
246,
|
|
||||||
10.5572,
|
|
||||||
36,
|
|
||||||
8,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
7.6246,
|
|
||||||
255,
|
|
||||||
5.2786,
|
|
||||||
18,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
19.0616,
|
|
||||||
241,
|
|
||||||
75.9531,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
19.0616,
|
|
||||||
267,
|
|
||||||
21.6734,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
43.081,
|
|
||||||
267,
|
|
||||||
1.0585,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
46.4855,
|
|
||||||
267,
|
|
||||||
26.8649,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "dash-sessions",
|
|
||||||
"viewportWidth": 726,
|
|
||||||
"width": 726,
|
|
||||||
"height": 319,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
319,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.6171,
|
|
||||||
19,
|
|
||||||
16.4106,
|
|
||||||
17,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
80.0749,
|
|
||||||
15,
|
|
||||||
17.308,
|
|
||||||
37,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0.1377,
|
|
||||||
67,
|
|
||||||
99.7245,
|
|
||||||
85,
|
|
||||||
8,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.4435,
|
|
||||||
89,
|
|
||||||
5.5096,
|
|
||||||
40,
|
|
||||||
8,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.9587,
|
|
||||||
100,
|
|
||||||
2.4793,
|
|
||||||
18,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
34.5278,
|
|
||||||
83,
|
|
||||||
9.1662,
|
|
||||||
27,
|
|
||||||
9999
|
|
||||||
],
|
|
||||||
[
|
|
||||||
11.157,
|
|
||||||
114,
|
|
||||||
11.0279,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.2868,
|
|
||||||
114,
|
|
||||||
0.5381,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.9268,
|
|
||||||
114,
|
|
||||||
13.6686,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.4435,
|
|
||||||
173,
|
|
||||||
5.5096,
|
|
||||||
40,
|
|
||||||
8,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.9587,
|
|
||||||
184,
|
|
||||||
2.4793,
|
|
||||||
18,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
11.157,
|
|
||||||
168,
|
|
||||||
85.3994,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
11.157,
|
|
||||||
198,
|
|
||||||
11.0279,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.2868,
|
|
||||||
198,
|
|
||||||
0.5381,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.9268,
|
|
||||||
198,
|
|
||||||
13.6686,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.4435,
|
|
||||||
257,
|
|
||||||
5.5096,
|
|
||||||
40,
|
|
||||||
8,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.9587,
|
|
||||||
268,
|
|
||||||
2.4793,
|
|
||||||
18,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
11.157,
|
|
||||||
251,
|
|
||||||
85.3994,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
11.157,
|
|
||||||
281,
|
|
||||||
11.0279,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.2868,
|
|
||||||
281,
|
|
||||||
0.5381,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.9268,
|
|
||||||
281,
|
|
||||||
13.6686,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "dash-sessions",
|
|
||||||
"viewportWidth": 484,
|
|
||||||
"width": 484,
|
|
||||||
"height": 309,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
309,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.9256,
|
|
||||||
15,
|
|
||||||
24.6158,
|
|
||||||
17,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.1785,
|
|
||||||
15,
|
|
||||||
23.8959,
|
|
||||||
29,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0.2066,
|
|
||||||
59,
|
|
||||||
99.5868,
|
|
||||||
83,
|
|
||||||
8,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
5.1653,
|
|
||||||
80,
|
|
||||||
8.2645,
|
|
||||||
40,
|
|
||||||
8,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
7.438,
|
|
||||||
91,
|
|
||||||
3.719,
|
|
||||||
18,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.7917,
|
|
||||||
76,
|
|
||||||
12.9358,
|
|
||||||
24,
|
|
||||||
9999
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.7355,
|
|
||||||
105,
|
|
||||||
16.5418,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
34.9303,
|
|
||||||
105,
|
|
||||||
0.8071,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
37.3902,
|
|
||||||
105,
|
|
||||||
20.503,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
5.1653,
|
|
||||||
164,
|
|
||||||
8.2645,
|
|
||||||
40,
|
|
||||||
8,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
7.438,
|
|
||||||
175,
|
|
||||||
3.719,
|
|
||||||
18,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.7355,
|
|
||||||
158,
|
|
||||||
78.0992,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.7355,
|
|
||||||
188,
|
|
||||||
16.5418,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
34.9303,
|
|
||||||
188,
|
|
||||||
0.8071,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
37.3902,
|
|
||||||
188,
|
|
||||||
20.503,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
5.1653,
|
|
||||||
247,
|
|
||||||
8.2645,
|
|
||||||
40,
|
|
||||||
8,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
7.438,
|
|
||||||
258,
|
|
||||||
3.719,
|
|
||||||
18,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.7355,
|
|
||||||
242,
|
|
||||||
78.0992,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.7355,
|
|
||||||
271,
|
|
||||||
16.5418,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
34.9303,
|
|
||||||
271,
|
|
||||||
0.8071,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
37.3902,
|
|
||||||
271,
|
|
||||||
20.503,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "e057ca7b36a30c5971a4225ec3ad4680"
|
|
||||||
}
|
|
||||||
@@ -1,707 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "invoice-detail",
|
|
||||||
"viewportWidth": 351,
|
|
||||||
"width": 351,
|
|
||||||
"height": 466,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
3.4188,
|
|
||||||
12,
|
|
||||||
5.698,
|
|
||||||
20,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
14.8148,
|
|
||||||
9,
|
|
||||||
30.0881,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
52,
|
|
||||||
30.6713,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
34.0901,
|
|
||||||
52,
|
|
||||||
31.2455,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
68.7545,
|
|
||||||
52,
|
|
||||||
31.2455,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
112,
|
|
||||||
100,
|
|
||||||
152,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
125,
|
|
||||||
44.0171,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
154,
|
|
||||||
44.0171,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.2792,
|
|
||||||
125,
|
|
||||||
44.0171,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.2792,
|
|
||||||
154,
|
|
||||||
44.0171,
|
|
||||||
27,
|
|
||||||
9999
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
197,
|
|
||||||
44.0171,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
226,
|
|
||||||
44.0171,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.2792,
|
|
||||||
197,
|
|
||||||
44.0171,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.2792,
|
|
||||||
226,
|
|
||||||
44.0171,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
280,
|
|
||||||
100,
|
|
||||||
186,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
293,
|
|
||||||
92.5926,
|
|
||||||
17,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
322,
|
|
||||||
33.3066,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
37.0103,
|
|
||||||
322,
|
|
||||||
35.1718,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.1822,
|
|
||||||
322,
|
|
||||||
36.9836,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.1658,
|
|
||||||
322,
|
|
||||||
36.9881,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
353,
|
|
||||||
33.3066,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
37.0103,
|
|
||||||
353,
|
|
||||||
35.1718,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.1822,
|
|
||||||
353,
|
|
||||||
36.9836,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.1658,
|
|
||||||
353,
|
|
||||||
36.9881,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
387,
|
|
||||||
33.3066,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
37.0103,
|
|
||||||
387,
|
|
||||||
35.1718,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.1822,
|
|
||||||
387,
|
|
||||||
36.9836,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.1658,
|
|
||||||
387,
|
|
||||||
36.9881,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
420,
|
|
||||||
33.3066,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
37.0103,
|
|
||||||
420,
|
|
||||||
35.1718,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.1822,
|
|
||||||
420,
|
|
||||||
36.9836,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.1658,
|
|
||||||
420,
|
|
||||||
36.9881,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "invoice-detail",
|
|
||||||
"viewportWidth": 736,
|
|
||||||
"width": 736,
|
|
||||||
"height": 444,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
1.6304,
|
|
||||||
12,
|
|
||||||
2.7174,
|
|
||||||
20,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
7.0652,
|
|
||||||
7,
|
|
||||||
17.5378,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.9692,
|
|
||||||
0,
|
|
||||||
9.1733,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.7729,
|
|
||||||
0,
|
|
||||||
13.6379,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
89.0413,
|
|
||||||
0,
|
|
||||||
10.9587,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
60,
|
|
||||||
100,
|
|
||||||
164,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
79,
|
|
||||||
46.3315,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
108,
|
|
||||||
46.3315,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.087,
|
|
||||||
79,
|
|
||||||
46.3315,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.087,
|
|
||||||
108,
|
|
||||||
46.3315,
|
|
||||||
27,
|
|
||||||
9999
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
151,
|
|
||||||
46.3315,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
180,
|
|
||||||
46.3315,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.087,
|
|
||||||
151,
|
|
||||||
46.3315,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.087,
|
|
||||||
180,
|
|
||||||
46.3315,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
240,
|
|
||||||
100,
|
|
||||||
204,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
259,
|
|
||||||
94.837,
|
|
||||||
17,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
288,
|
|
||||||
22.3803,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.9618,
|
|
||||||
288,
|
|
||||||
23.5882,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.55,
|
|
||||||
288,
|
|
||||||
24.431,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.9811,
|
|
||||||
288,
|
|
||||||
24.4374,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
321,
|
|
||||||
22.3803,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.9618,
|
|
||||||
321,
|
|
||||||
23.5882,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.55,
|
|
||||||
321,
|
|
||||||
24.431,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.9811,
|
|
||||||
321,
|
|
||||||
24.4374,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
356,
|
|
||||||
22.3803,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.9618,
|
|
||||||
356,
|
|
||||||
23.5882,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.55,
|
|
||||||
356,
|
|
||||||
24.431,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.9811,
|
|
||||||
356,
|
|
||||||
24.4374,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
391,
|
|
||||||
22.3803,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.9618,
|
|
||||||
391,
|
|
||||||
23.5882,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.55,
|
|
||||||
391,
|
|
||||||
24.431,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.9811,
|
|
||||||
391,
|
|
||||||
24.4374,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "invoice-detail",
|
|
||||||
"viewportWidth": 996,
|
|
||||||
"width": 996,
|
|
||||||
"height": 457,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0.6024,
|
|
||||||
7,
|
|
||||||
2.008,
|
|
||||||
20,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.0161,
|
|
||||||
2,
|
|
||||||
12.9597,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.8407,
|
|
||||||
0,
|
|
||||||
6.3771,
|
|
||||||
34,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.4226,
|
|
||||||
0,
|
|
||||||
9.6762,
|
|
||||||
34,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
92.3036,
|
|
||||||
0,
|
|
||||||
7.6964,
|
|
||||||
34,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
50,
|
|
||||||
100,
|
|
||||||
160,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
69,
|
|
||||||
47.2892,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
96,
|
|
||||||
47.2892,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
50.8032,
|
|
||||||
69,
|
|
||||||
47.2892,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
50.8032,
|
|
||||||
96,
|
|
||||||
47.2892,
|
|
||||||
24,
|
|
||||||
9999
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
138,
|
|
||||||
47.2892,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
165,
|
|
||||||
47.2892,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
50.8032,
|
|
||||||
138,
|
|
||||||
47.2892,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
50.8032,
|
|
||||||
165,
|
|
||||||
47.2892,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
226,
|
|
||||||
100,
|
|
||||||
232,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
245,
|
|
||||||
96.1847,
|
|
||||||
17,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
273,
|
|
||||||
22.9606,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.8682,
|
|
||||||
273,
|
|
||||||
24.065,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.9332,
|
|
||||||
273,
|
|
||||||
24.578,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.5112,
|
|
||||||
273,
|
|
||||||
24.5811,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
311,
|
|
||||||
22.9606,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.8682,
|
|
||||||
311,
|
|
||||||
24.065,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.9332,
|
|
||||||
311,
|
|
||||||
24.578,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.5112,
|
|
||||||
311,
|
|
||||||
24.5811,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
354,
|
|
||||||
22.9606,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.8682,
|
|
||||||
354,
|
|
||||||
24.065,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.9332,
|
|
||||||
354,
|
|
||||||
24.578,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.5112,
|
|
||||||
354,
|
|
||||||
24.5811,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
396,
|
|
||||||
22.9606,
|
|
||||||
42,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.8682,
|
|
||||||
396,
|
|
||||||
24.065,
|
|
||||||
42,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.9332,
|
|
||||||
396,
|
|
||||||
24.578,
|
|
||||||
42,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.5112,
|
|
||||||
396,
|
|
||||||
24.5811,
|
|
||||||
42,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "934452d45a0bef9320dc379fb3f43bb5"
|
|
||||||
}
|
|
||||||
@@ -1,599 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "leave-approval",
|
|
||||||
"viewportWidth": 351,
|
|
||||||
"width": 351,
|
|
||||||
"height": 294,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
26,
|
|
||||||
100,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
61,
|
|
||||||
100,
|
|
||||||
233,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
74,
|
|
||||||
19.2753,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
22.979,
|
|
||||||
74,
|
|
||||||
18.7812,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.7601,
|
|
||||||
74,
|
|
||||||
23.0502,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.8104,
|
|
||||||
74,
|
|
||||||
23.0502,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.8606,
|
|
||||||
74,
|
|
||||||
9.4373,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
97.2979,
|
|
||||||
74,
|
|
||||||
54.4471,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
105,
|
|
||||||
19.2753,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
22.979,
|
|
||||||
105,
|
|
||||||
18.7812,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.7601,
|
|
||||||
105,
|
|
||||||
23.0502,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.8104,
|
|
||||||
105,
|
|
||||||
23.0502,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.8606,
|
|
||||||
105,
|
|
||||||
9.4373,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
97.2979,
|
|
||||||
105,
|
|
||||||
54.4471,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
159,
|
|
||||||
19.2753,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
22.979,
|
|
||||||
159,
|
|
||||||
18.7812,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.7601,
|
|
||||||
159,
|
|
||||||
23.0502,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.8104,
|
|
||||||
159,
|
|
||||||
23.0502,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.8606,
|
|
||||||
159,
|
|
||||||
9.4373,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
97.2979,
|
|
||||||
159,
|
|
||||||
54.4471,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
213,
|
|
||||||
19.2753,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
22.979,
|
|
||||||
213,
|
|
||||||
18.7812,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.7601,
|
|
||||||
213,
|
|
||||||
23.0502,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.8104,
|
|
||||||
213,
|
|
||||||
23.0502,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.8606,
|
|
||||||
213,
|
|
||||||
9.4373,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
97.2979,
|
|
||||||
213,
|
|
||||||
54.4471,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "leave-approval",
|
|
||||||
"viewportWidth": 736,
|
|
||||||
"width": 736,
|
|
||||||
"height": 299,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
29.4264,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
29.4264,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
232,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
86,
|
|
||||||
12.6911,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
15.2726,
|
|
||||||
86,
|
|
||||||
12.3769,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.6495,
|
|
||||||
86,
|
|
||||||
15.0921,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
42.7416,
|
|
||||||
86,
|
|
||||||
15.0921,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
57.8337,
|
|
||||||
86,
|
|
||||||
6.4856,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.3194,
|
|
||||||
86,
|
|
||||||
33.0991,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
119,
|
|
||||||
12.6911,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
15.2726,
|
|
||||||
119,
|
|
||||||
12.3769,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.6495,
|
|
||||||
119,
|
|
||||||
15.0921,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
42.7416,
|
|
||||||
119,
|
|
||||||
15.0921,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
57.8337,
|
|
||||||
119,
|
|
||||||
6.4856,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.3194,
|
|
||||||
119,
|
|
||||||
33.0991,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
173,
|
|
||||||
12.6911,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
15.2726,
|
|
||||||
173,
|
|
||||||
12.3769,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.6495,
|
|
||||||
173,
|
|
||||||
15.0921,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
42.7416,
|
|
||||||
173,
|
|
||||||
15.0921,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
57.8337,
|
|
||||||
173,
|
|
||||||
6.4856,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.3194,
|
|
||||||
173,
|
|
||||||
33.0991,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
227,
|
|
||||||
12.6911,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
15.2726,
|
|
||||||
227,
|
|
||||||
12.3769,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.6495,
|
|
||||||
227,
|
|
||||||
15.0921,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
42.7416,
|
|
||||||
227,
|
|
||||||
15.0921,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
57.8337,
|
|
||||||
227,
|
|
||||||
6.4856,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.3194,
|
|
||||||
227,
|
|
||||||
33.0991,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "leave-approval",
|
|
||||||
"viewportWidth": 996,
|
|
||||||
"width": 996,
|
|
||||||
"height": 299,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
21.7448,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
21.7448,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
232,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
86,
|
|
||||||
13.8664,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
15.774,
|
|
||||||
86,
|
|
||||||
13.5589,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
29.333,
|
|
||||||
86,
|
|
||||||
16.196,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.529,
|
|
||||||
86,
|
|
||||||
16.196,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
61.725,
|
|
||||||
86,
|
|
||||||
7.8878,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.6128,
|
|
||||||
86,
|
|
||||||
28.4795,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
124,
|
|
||||||
13.8664,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
15.774,
|
|
||||||
124,
|
|
||||||
13.5589,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
29.333,
|
|
||||||
124,
|
|
||||||
16.196,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.529,
|
|
||||||
124,
|
|
||||||
16.196,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
61.725,
|
|
||||||
124,
|
|
||||||
7.8878,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.6128,
|
|
||||||
124,
|
|
||||||
28.4795,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
176,
|
|
||||||
13.8664,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
15.774,
|
|
||||||
176,
|
|
||||||
13.5589,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
29.333,
|
|
||||||
176,
|
|
||||||
16.196,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.529,
|
|
||||||
176,
|
|
||||||
16.196,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
61.725,
|
|
||||||
176,
|
|
||||||
7.8878,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.6128,
|
|
||||||
176,
|
|
||||||
28.4795,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
228,
|
|
||||||
13.8664,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
15.774,
|
|
||||||
228,
|
|
||||||
13.5589,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
29.333,
|
|
||||||
228,
|
|
||||||
16.196,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.529,
|
|
||||||
228,
|
|
||||||
16.196,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
61.725,
|
|
||||||
228,
|
|
||||||
7.8878,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.6128,
|
|
||||||
228,
|
|
||||||
28.4795,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "4b74917f659334073252a738cfa9c4ac"
|
|
||||||
}
|
|
||||||
@@ -1,704 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "leave-requests",
|
|
||||||
"viewportWidth": 351,
|
|
||||||
"width": 351,
|
|
||||||
"height": 367,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
26,
|
|
||||||
100,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
53,
|
|
||||||
100,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
113,
|
|
||||||
100,
|
|
||||||
254,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
126,
|
|
||||||
21.1806,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.8843,
|
|
||||||
126,
|
|
||||||
20.6375,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.5217,
|
|
||||||
126,
|
|
||||||
25.3294,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.8511,
|
|
||||||
126,
|
|
||||||
25.3294,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
96.1806,
|
|
||||||
126,
|
|
||||||
10.3677,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
106.5483,
|
|
||||||
126,
|
|
||||||
20.7977,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
127.346,
|
|
||||||
126,
|
|
||||||
18.8079,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
157,
|
|
||||||
21.1806,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.8843,
|
|
||||||
157,
|
|
||||||
20.6375,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.5217,
|
|
||||||
157,
|
|
||||||
25.3294,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.8511,
|
|
||||||
157,
|
|
||||||
25.3294,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
96.1806,
|
|
||||||
157,
|
|
||||||
10.3677,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
106.5483,
|
|
||||||
157,
|
|
||||||
20.7977,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
127.346,
|
|
||||||
157,
|
|
||||||
18.8079,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
218,
|
|
||||||
21.1806,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.8843,
|
|
||||||
218,
|
|
||||||
20.6375,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.5217,
|
|
||||||
218,
|
|
||||||
25.3294,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.8511,
|
|
||||||
218,
|
|
||||||
25.3294,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
96.1806,
|
|
||||||
218,
|
|
||||||
10.3677,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
106.5483,
|
|
||||||
218,
|
|
||||||
20.7977,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
127.346,
|
|
||||||
218,
|
|
||||||
18.8079,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
279,
|
|
||||||
21.1806,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.8843,
|
|
||||||
279,
|
|
||||||
20.6375,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.5217,
|
|
||||||
279,
|
|
||||||
25.3294,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.8511,
|
|
||||||
279,
|
|
||||||
25.3294,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
96.1806,
|
|
||||||
279,
|
|
||||||
10.3677,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
106.5483,
|
|
||||||
279,
|
|
||||||
20.7977,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
127.346,
|
|
||||||
279,
|
|
||||||
18.8079,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "leave-requests",
|
|
||||||
"viewportWidth": 736,
|
|
||||||
"width": 736,
|
|
||||||
"height": 320,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
27.1888,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
27.1888,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
83.6914,
|
|
||||||
4,
|
|
||||||
16.3086,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
253,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
86,
|
|
||||||
14.313,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.8945,
|
|
||||||
86,
|
|
||||||
13.9585,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.853,
|
|
||||||
86,
|
|
||||||
17.0219,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.8749,
|
|
||||||
86,
|
|
||||||
17.0219,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.8968,
|
|
||||||
86,
|
|
||||||
7.3157,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.2126,
|
|
||||||
86,
|
|
||||||
13.2006,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4131,
|
|
||||||
86,
|
|
||||||
12.0053,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
119,
|
|
||||||
14.313,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.8945,
|
|
||||||
119,
|
|
||||||
13.9585,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.853,
|
|
||||||
119,
|
|
||||||
17.0219,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.8749,
|
|
||||||
119,
|
|
||||||
17.0219,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.8968,
|
|
||||||
119,
|
|
||||||
7.3157,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.2126,
|
|
||||||
119,
|
|
||||||
13.2006,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4131,
|
|
||||||
119,
|
|
||||||
12.0053,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
180,
|
|
||||||
14.313,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.8945,
|
|
||||||
180,
|
|
||||||
13.9585,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.853,
|
|
||||||
180,
|
|
||||||
17.0219,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.8749,
|
|
||||||
180,
|
|
||||||
17.0219,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.8968,
|
|
||||||
180,
|
|
||||||
7.3157,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.2126,
|
|
||||||
180,
|
|
||||||
13.2006,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4131,
|
|
||||||
180,
|
|
||||||
12.0053,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
241,
|
|
||||||
14.313,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.8945,
|
|
||||||
241,
|
|
||||||
13.9585,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.853,
|
|
||||||
241,
|
|
||||||
17.0219,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.8749,
|
|
||||||
241,
|
|
||||||
17.0219,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.8968,
|
|
||||||
241,
|
|
||||||
7.3157,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.2126,
|
|
||||||
241,
|
|
||||||
13.2006,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4131,
|
|
||||||
241,
|
|
||||||
12.0053,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "leave-requests",
|
|
||||||
"viewportWidth": 996,
|
|
||||||
"width": 996,
|
|
||||||
"height": 308,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
20.0913,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
20.0913,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
88.3503,
|
|
||||||
10,
|
|
||||||
11.6497,
|
|
||||||
32,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
241,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
86,
|
|
||||||
14.9787,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.8863,
|
|
||||||
86,
|
|
||||||
14.6461,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
31.5324,
|
|
||||||
86,
|
|
||||||
17.495,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.0274,
|
|
||||||
86,
|
|
||||||
17.495,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
66.5223,
|
|
||||||
86,
|
|
||||||
8.52,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
75.0424,
|
|
||||||
86,
|
|
||||||
12.74,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.7824,
|
|
||||||
86,
|
|
||||||
10.31,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
124,
|
|
||||||
14.9787,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.8863,
|
|
||||||
124,
|
|
||||||
14.6461,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
31.5324,
|
|
||||||
124,
|
|
||||||
17.495,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.0274,
|
|
||||||
124,
|
|
||||||
17.495,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
66.5223,
|
|
||||||
124,
|
|
||||||
8.52,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
75.0424,
|
|
||||||
124,
|
|
||||||
12.74,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.7824,
|
|
||||||
124,
|
|
||||||
10.31,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
179,
|
|
||||||
14.9787,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.8863,
|
|
||||||
179,
|
|
||||||
14.6461,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
31.5324,
|
|
||||||
179,
|
|
||||||
17.495,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.0274,
|
|
||||||
179,
|
|
||||||
17.495,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
66.5223,
|
|
||||||
179,
|
|
||||||
8.52,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
75.0424,
|
|
||||||
179,
|
|
||||||
12.74,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.7824,
|
|
||||||
179,
|
|
||||||
10.31,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
234,
|
|
||||||
14.9787,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.8863,
|
|
||||||
234,
|
|
||||||
14.6461,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
31.5324,
|
|
||||||
234,
|
|
||||||
17.495,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.0274,
|
|
||||||
234,
|
|
||||||
17.495,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
66.5223,
|
|
||||||
234,
|
|
||||||
8.52,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
75.0424,
|
|
||||||
234,
|
|
||||||
12.74,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.7824,
|
|
||||||
234,
|
|
||||||
10.31,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "125231cbf4c6abc4e73bb48732dc9353"
|
|
||||||
}
|
|
||||||
@@ -1,620 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "offer-detail",
|
|
||||||
"viewportWidth": 351,
|
|
||||||
"width": 351,
|
|
||||||
"height": 483,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
12.5356,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
52,
|
|
||||||
100,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
86,
|
|
||||||
100,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
146,
|
|
||||||
100,
|
|
||||||
338,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
159,
|
|
||||||
44.0171,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
187,
|
|
||||||
44.0171,
|
|
||||||
46,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.2792,
|
|
||||||
159,
|
|
||||||
44.0171,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.2792,
|
|
||||||
187,
|
|
||||||
44.0171,
|
|
||||||
46,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
249,
|
|
||||||
44.0171,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
278,
|
|
||||||
44.0171,
|
|
||||||
46,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.2792,
|
|
||||||
249,
|
|
||||||
44.0171,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.2792,
|
|
||||||
278,
|
|
||||||
44.0171,
|
|
||||||
46,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
339,
|
|
||||||
34.4062,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.1099,
|
|
||||||
339,
|
|
||||||
34.8157,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.9256,
|
|
||||||
339,
|
|
||||||
36.6097,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.5353,
|
|
||||||
339,
|
|
||||||
36.6186,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
370,
|
|
||||||
34.4062,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.1099,
|
|
||||||
370,
|
|
||||||
34.8157,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.9256,
|
|
||||||
370,
|
|
||||||
36.6097,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.5353,
|
|
||||||
370,
|
|
||||||
36.6186,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
404,
|
|
||||||
34.4062,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.1099,
|
|
||||||
404,
|
|
||||||
34.8157,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.9256,
|
|
||||||
404,
|
|
||||||
36.6097,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.5353,
|
|
||||||
404,
|
|
||||||
36.6186,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
437,
|
|
||||||
34.4062,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.1099,
|
|
||||||
437,
|
|
||||||
34.8157,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.9256,
|
|
||||||
437,
|
|
||||||
36.6097,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.5353,
|
|
||||||
437,
|
|
||||||
36.6186,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "offer-detail",
|
|
||||||
"viewportWidth": 736,
|
|
||||||
"width": 736,
|
|
||||||
"height": 416,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
5.9783,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.4829,
|
|
||||||
7,
|
|
||||||
19.8412,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
90.8267,
|
|
||||||
0,
|
|
||||||
9.1733,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
60,
|
|
||||||
100,
|
|
||||||
356,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
79,
|
|
||||||
46.3315,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
108,
|
|
||||||
46.3315,
|
|
||||||
46,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.087,
|
|
||||||
79,
|
|
||||||
46.3315,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.087,
|
|
||||||
108,
|
|
||||||
46.3315,
|
|
||||||
46,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
169,
|
|
||||||
46.3315,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
198,
|
|
||||||
46.3315,
|
|
||||||
46,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.087,
|
|
||||||
169,
|
|
||||||
46.3315,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.087,
|
|
||||||
198,
|
|
||||||
46.3315,
|
|
||||||
46,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
260,
|
|
||||||
22.8558,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.4373,
|
|
||||||
260,
|
|
||||||
23.4333,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.8706,
|
|
||||||
260,
|
|
||||||
24.2718,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.1424,
|
|
||||||
260,
|
|
||||||
24.2761,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
292,
|
|
||||||
22.8558,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.4373,
|
|
||||||
292,
|
|
||||||
23.4333,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.8706,
|
|
||||||
292,
|
|
||||||
24.2718,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.1424,
|
|
||||||
292,
|
|
||||||
24.2761,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
327,
|
|
||||||
22.8558,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.4373,
|
|
||||||
327,
|
|
||||||
23.4333,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.8706,
|
|
||||||
327,
|
|
||||||
24.2718,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.1424,
|
|
||||||
327,
|
|
||||||
24.2761,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
362,
|
|
||||||
22.8558,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.4373,
|
|
||||||
362,
|
|
||||||
23.4333,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.8706,
|
|
||||||
362,
|
|
||||||
24.2718,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.1424,
|
|
||||||
362,
|
|
||||||
24.2761,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "offer-detail",
|
|
||||||
"viewportWidth": 996,
|
|
||||||
"width": 996,
|
|
||||||
"height": 419,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
3.2129,
|
|
||||||
32,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.0878,
|
|
||||||
1,
|
|
||||||
14.6618,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
93.6229,
|
|
||||||
0,
|
|
||||||
6.3771,
|
|
||||||
32,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
48,
|
|
||||||
100,
|
|
||||||
371,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
67,
|
|
||||||
47.2892,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
94,
|
|
||||||
47.2892,
|
|
||||||
41,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
50.8032,
|
|
||||||
67,
|
|
||||||
47.2892,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
50.8032,
|
|
||||||
94,
|
|
||||||
47.2892,
|
|
||||||
41,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
151,
|
|
||||||
47.2892,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
178,
|
|
||||||
47.2892,
|
|
||||||
41,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
50.8032,
|
|
||||||
151,
|
|
||||||
47.2892,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
50.8032,
|
|
||||||
178,
|
|
||||||
47.2892,
|
|
||||||
41,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
235,
|
|
||||||
23.2194,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.1271,
|
|
||||||
235,
|
|
||||||
23.9787,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.1058,
|
|
||||||
235,
|
|
||||||
24.4917,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.5975,
|
|
||||||
235,
|
|
||||||
24.4949,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
273,
|
|
||||||
23.2194,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.1271,
|
|
||||||
273,
|
|
||||||
23.9787,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.1058,
|
|
||||||
273,
|
|
||||||
24.4917,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.5975,
|
|
||||||
273,
|
|
||||||
24.4949,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
316,
|
|
||||||
23.2194,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.1271,
|
|
||||||
316,
|
|
||||||
23.9787,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.1058,
|
|
||||||
316,
|
|
||||||
24.4917,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.5975,
|
|
||||||
316,
|
|
||||||
24.4949,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
358,
|
|
||||||
23.2194,
|
|
||||||
42,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.1271,
|
|
||||||
358,
|
|
||||||
23.9787,
|
|
||||||
42,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.1058,
|
|
||||||
358,
|
|
||||||
24.4917,
|
|
||||||
42,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.5975,
|
|
||||||
358,
|
|
||||||
24.4949,
|
|
||||||
42,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "67676fde7dd5c432922d819fc9bf48db"
|
|
||||||
}
|
|
||||||
@@ -1,641 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "offers-customers",
|
|
||||||
"viewportWidth": 351,
|
|
||||||
"width": 351,
|
|
||||||
"height": 549,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
26,
|
|
||||||
100,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
53,
|
|
||||||
100,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
113,
|
|
||||||
100,
|
|
||||||
436,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
126,
|
|
||||||
92.5926,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
186,
|
|
||||||
34.562,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.2657,
|
|
||||||
186,
|
|
||||||
23.7936,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.0593,
|
|
||||||
186,
|
|
||||||
32.4653,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
94.5246,
|
|
||||||
186,
|
|
||||||
51.6293,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
217,
|
|
||||||
34.562,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.2657,
|
|
||||||
217,
|
|
||||||
23.7936,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.0593,
|
|
||||||
217,
|
|
||||||
32.4653,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
94.5246,
|
|
||||||
217,
|
|
||||||
51.6293,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
278,
|
|
||||||
34.562,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.2657,
|
|
||||||
278,
|
|
||||||
23.7936,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.0593,
|
|
||||||
278,
|
|
||||||
32.4653,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
94.5246,
|
|
||||||
278,
|
|
||||||
51.6293,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
339,
|
|
||||||
34.562,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.2657,
|
|
||||||
339,
|
|
||||||
23.7936,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.0593,
|
|
||||||
339,
|
|
||||||
32.4653,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
94.5246,
|
|
||||||
339,
|
|
||||||
51.6293,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
400,
|
|
||||||
34.562,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.2657,
|
|
||||||
400,
|
|
||||||
23.7936,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.0593,
|
|
||||||
400,
|
|
||||||
32.4653,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
94.5246,
|
|
||||||
400,
|
|
||||||
51.6293,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
461,
|
|
||||||
34.562,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.2657,
|
|
||||||
461,
|
|
||||||
23.7936,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.0593,
|
|
||||||
461,
|
|
||||||
32.4653,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
94.5246,
|
|
||||||
461,
|
|
||||||
51.6293,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "offers-customers",
|
|
||||||
"viewportWidth": 736,
|
|
||||||
"width": 736,
|
|
||||||
"height": 502,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
12.9458,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
12.9458,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
80.6322,
|
|
||||||
4,
|
|
||||||
19.3678,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
435,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
86,
|
|
||||||
94.837,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
146,
|
|
||||||
23.2868,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.8683,
|
|
||||||
146,
|
|
||||||
16.453,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
42.3212,
|
|
||||||
146,
|
|
||||||
21.9175,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.2387,
|
|
||||||
146,
|
|
||||||
33.1798,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
179,
|
|
||||||
23.2868,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.8683,
|
|
||||||
179,
|
|
||||||
16.453,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
42.3212,
|
|
||||||
179,
|
|
||||||
21.9175,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.2387,
|
|
||||||
179,
|
|
||||||
33.1798,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
240,
|
|
||||||
23.2868,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.8683,
|
|
||||||
240,
|
|
||||||
16.453,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
42.3212,
|
|
||||||
240,
|
|
||||||
21.9175,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.2387,
|
|
||||||
240,
|
|
||||||
33.1798,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
301,
|
|
||||||
23.2868,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.8683,
|
|
||||||
301,
|
|
||||||
16.453,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
42.3212,
|
|
||||||
301,
|
|
||||||
21.9175,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.2387,
|
|
||||||
301,
|
|
||||||
33.1798,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
362,
|
|
||||||
23.2868,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.8683,
|
|
||||||
362,
|
|
||||||
16.453,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
42.3212,
|
|
||||||
362,
|
|
||||||
21.9175,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.2387,
|
|
||||||
362,
|
|
||||||
33.1798,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
423,
|
|
||||||
23.2868,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.8683,
|
|
||||||
423,
|
|
||||||
16.453,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
42.3212,
|
|
||||||
423,
|
|
||||||
21.9175,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.2387,
|
|
||||||
423,
|
|
||||||
33.1798,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "offers-customers",
|
|
||||||
"viewportWidth": 996,
|
|
||||||
"width": 996,
|
|
||||||
"height": 470,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
9.5664,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
9.5664,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
86.0897,
|
|
||||||
10,
|
|
||||||
13.9103,
|
|
||||||
32,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
403,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
86,
|
|
||||||
96.1847,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
138,
|
|
||||||
25.673,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.5806,
|
|
||||||
138,
|
|
||||||
19.0904,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
46.6711,
|
|
||||||
138,
|
|
||||||
24.3254,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.9965,
|
|
||||||
138,
|
|
||||||
27.0959,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
176,
|
|
||||||
25.673,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.5806,
|
|
||||||
176,
|
|
||||||
19.0904,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
46.6711,
|
|
||||||
176,
|
|
||||||
24.3254,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.9965,
|
|
||||||
176,
|
|
||||||
27.0959,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
231,
|
|
||||||
25.673,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.5806,
|
|
||||||
231,
|
|
||||||
19.0904,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
46.6711,
|
|
||||||
231,
|
|
||||||
24.3254,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.9965,
|
|
||||||
231,
|
|
||||||
27.0959,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
286,
|
|
||||||
25.673,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.5806,
|
|
||||||
286,
|
|
||||||
19.0904,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
46.6711,
|
|
||||||
286,
|
|
||||||
24.3254,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.9965,
|
|
||||||
286,
|
|
||||||
27.0959,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
341,
|
|
||||||
25.673,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.5806,
|
|
||||||
341,
|
|
||||||
19.0904,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
46.6711,
|
|
||||||
341,
|
|
||||||
24.3254,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.9965,
|
|
||||||
341,
|
|
||||||
27.0959,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
396,
|
|
||||||
25.673,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.5806,
|
|
||||||
396,
|
|
||||||
19.0904,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
46.6711,
|
|
||||||
396,
|
|
||||||
24.3254,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.9965,
|
|
||||||
396,
|
|
||||||
27.0959,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "63b2dec2b6ceb84d931a000ab8b669dd"
|
|
||||||
}
|
|
||||||
@@ -1,452 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "offers-templates",
|
|
||||||
"viewportWidth": 351,
|
|
||||||
"width": 351,
|
|
||||||
"height": 436,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
436,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
13,
|
|
||||||
92.5926,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
73,
|
|
||||||
39.659,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
43.3627,
|
|
||||||
73,
|
|
||||||
39.6857,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
83.0484,
|
|
||||||
73,
|
|
||||||
63.1054,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
104,
|
|
||||||
39.659,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
43.3627,
|
|
||||||
104,
|
|
||||||
39.6857,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
83.0484,
|
|
||||||
104,
|
|
||||||
63.1054,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
165,
|
|
||||||
39.659,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
43.3627,
|
|
||||||
165,
|
|
||||||
39.6857,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
83.0484,
|
|
||||||
165,
|
|
||||||
63.1054,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
226,
|
|
||||||
39.659,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
43.3627,
|
|
||||||
226,
|
|
||||||
39.6857,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
83.0484,
|
|
||||||
226,
|
|
||||||
63.1054,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
287,
|
|
||||||
39.659,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
43.3627,
|
|
||||||
287,
|
|
||||||
39.6857,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
83.0484,
|
|
||||||
287,
|
|
||||||
63.1054,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
348,
|
|
||||||
39.659,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
43.3627,
|
|
||||||
348,
|
|
||||||
39.6857,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
83.0484,
|
|
||||||
348,
|
|
||||||
63.1054,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "offers-templates",
|
|
||||||
"viewportWidth": 736,
|
|
||||||
"width": 736,
|
|
||||||
"height": 435,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
435,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
19,
|
|
||||||
94.837,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
79,
|
|
||||||
26.9744,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
29.5559,
|
|
||||||
79,
|
|
||||||
26.9977,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
56.5536,
|
|
||||||
79,
|
|
||||||
40.8649,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
112,
|
|
||||||
26.9744,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
29.5559,
|
|
||||||
112,
|
|
||||||
26.9977,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
56.5536,
|
|
||||||
112,
|
|
||||||
40.8649,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
173,
|
|
||||||
26.9744,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
29.5559,
|
|
||||||
173,
|
|
||||||
26.9977,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
56.5536,
|
|
||||||
173,
|
|
||||||
40.8649,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
234,
|
|
||||||
26.9744,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
29.5559,
|
|
||||||
234,
|
|
||||||
26.9977,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
56.5536,
|
|
||||||
234,
|
|
||||||
40.8649,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
295,
|
|
||||||
26.9744,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
29.5559,
|
|
||||||
295,
|
|
||||||
26.9977,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
56.5536,
|
|
||||||
295,
|
|
||||||
40.8649,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
356,
|
|
||||||
26.9744,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
29.5559,
|
|
||||||
356,
|
|
||||||
26.9977,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
56.5536,
|
|
||||||
356,
|
|
||||||
40.8649,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "offers-templates",
|
|
||||||
"viewportWidth": 996,
|
|
||||||
"width": 996,
|
|
||||||
"height": 403,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
403,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
19,
|
|
||||||
96.1847,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
71,
|
|
||||||
30.8719,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
32.7796,
|
|
||||||
71,
|
|
||||||
30.897,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
63.6766,
|
|
||||||
71,
|
|
||||||
34.4158,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
109,
|
|
||||||
30.8719,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
32.7796,
|
|
||||||
109,
|
|
||||||
30.897,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
63.6766,
|
|
||||||
109,
|
|
||||||
34.4158,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
164,
|
|
||||||
30.8719,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
32.7796,
|
|
||||||
164,
|
|
||||||
30.897,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
63.6766,
|
|
||||||
164,
|
|
||||||
34.4158,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
219,
|
|
||||||
30.8719,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
32.7796,
|
|
||||||
219,
|
|
||||||
30.897,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
63.6766,
|
|
||||||
219,
|
|
||||||
34.4158,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
274,
|
|
||||||
30.8719,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
32.7796,
|
|
||||||
274,
|
|
||||||
30.897,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
63.6766,
|
|
||||||
274,
|
|
||||||
34.4158,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
329,
|
|
||||||
30.8719,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
32.7796,
|
|
||||||
329,
|
|
||||||
30.897,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
63.6766,
|
|
||||||
329,
|
|
||||||
34.4158,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "5e5881859bd932a42345c69a6a30ca65"
|
|
||||||
}
|
|
||||||
@@ -1,872 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "offers",
|
|
||||||
"viewportWidth": 351,
|
|
||||||
"width": 351,
|
|
||||||
"height": 497,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
26,
|
|
||||||
100,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
61,
|
|
||||||
100,
|
|
||||||
436,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
74,
|
|
||||||
92.5926,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
134,
|
|
||||||
26.7495,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.4532,
|
|
||||||
134,
|
|
||||||
20.6019,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.055,
|
|
||||||
134,
|
|
||||||
21.4165,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.4715,
|
|
||||||
134,
|
|
||||||
23.0502,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
95.5217,
|
|
||||||
134,
|
|
||||||
23.0502,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
118.5719,
|
|
||||||
134,
|
|
||||||
30.7692,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
165,
|
|
||||||
26.7495,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.4532,
|
|
||||||
165,
|
|
||||||
20.6019,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.055,
|
|
||||||
165,
|
|
||||||
21.4165,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.4715,
|
|
||||||
165,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
95.5217,
|
|
||||||
165,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
118.5719,
|
|
||||||
165,
|
|
||||||
30.7692,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
226,
|
|
||||||
26.7495,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.4532,
|
|
||||||
226,
|
|
||||||
20.6019,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.055,
|
|
||||||
226,
|
|
||||||
21.4165,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.4715,
|
|
||||||
226,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
95.5217,
|
|
||||||
226,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
118.5719,
|
|
||||||
226,
|
|
||||||
30.7692,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
287,
|
|
||||||
26.7495,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.4532,
|
|
||||||
287,
|
|
||||||
20.6019,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.055,
|
|
||||||
287,
|
|
||||||
21.4165,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.4715,
|
|
||||||
287,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
95.5217,
|
|
||||||
287,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
118.5719,
|
|
||||||
287,
|
|
||||||
30.7692,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
348,
|
|
||||||
26.7495,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.4532,
|
|
||||||
348,
|
|
||||||
20.6019,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.055,
|
|
||||||
348,
|
|
||||||
21.4165,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.4715,
|
|
||||||
348,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
95.5217,
|
|
||||||
348,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
118.5719,
|
|
||||||
348,
|
|
||||||
30.7692,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
409,
|
|
||||||
26.7495,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.4532,
|
|
||||||
409,
|
|
||||||
20.6019,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.055,
|
|
||||||
409,
|
|
||||||
21.4165,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.4715,
|
|
||||||
409,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
95.5217,
|
|
||||||
409,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
118.5719,
|
|
||||||
409,
|
|
||||||
30.7692,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "offers",
|
|
||||||
"viewportWidth": 736,
|
|
||||||
"width": 736,
|
|
||||||
"height": 502,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
11.3451,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
11.3451,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
435,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
86,
|
|
||||||
94.837,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
146,
|
|
||||||
17.6779,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.2594,
|
|
||||||
146,
|
|
||||||
13.7101,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
33.9695,
|
|
||||||
146,
|
|
||||||
13.3301,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.2996,
|
|
||||||
146,
|
|
||||||
15.2917,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.5913,
|
|
||||||
146,
|
|
||||||
15.2917,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
77.883,
|
|
||||||
146,
|
|
||||||
19.5355,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
179,
|
|
||||||
17.6779,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.2594,
|
|
||||||
179,
|
|
||||||
13.7101,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
33.9695,
|
|
||||||
179,
|
|
||||||
13.3301,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.2996,
|
|
||||||
179,
|
|
||||||
15.2917,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.5913,
|
|
||||||
179,
|
|
||||||
15.2917,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
77.883,
|
|
||||||
179,
|
|
||||||
19.5355,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
240,
|
|
||||||
17.6779,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.2594,
|
|
||||||
240,
|
|
||||||
13.7101,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
33.9695,
|
|
||||||
240,
|
|
||||||
13.3301,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.2996,
|
|
||||||
240,
|
|
||||||
15.2917,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.5913,
|
|
||||||
240,
|
|
||||||
15.2917,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
77.883,
|
|
||||||
240,
|
|
||||||
19.5355,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
301,
|
|
||||||
17.6779,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.2594,
|
|
||||||
301,
|
|
||||||
13.7101,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
33.9695,
|
|
||||||
301,
|
|
||||||
13.3301,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.2996,
|
|
||||||
301,
|
|
||||||
15.2917,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.5913,
|
|
||||||
301,
|
|
||||||
15.2917,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
77.883,
|
|
||||||
301,
|
|
||||||
19.5355,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
362,
|
|
||||||
17.6779,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.2594,
|
|
||||||
362,
|
|
||||||
13.7101,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
33.9695,
|
|
||||||
362,
|
|
||||||
13.3301,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.2996,
|
|
||||||
362,
|
|
||||||
15.2917,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.5913,
|
|
||||||
362,
|
|
||||||
15.2917,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
77.883,
|
|
||||||
362,
|
|
||||||
19.5355,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
423,
|
|
||||||
17.6779,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.2594,
|
|
||||||
423,
|
|
||||||
13.7101,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
33.9695,
|
|
||||||
423,
|
|
||||||
13.3301,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.2996,
|
|
||||||
423,
|
|
||||||
15.2917,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.5913,
|
|
||||||
423,
|
|
||||||
15.2917,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
77.883,
|
|
||||||
423,
|
|
||||||
19.5355,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "offers",
|
|
||||||
"viewportWidth": 996,
|
|
||||||
"width": 996,
|
|
||||||
"height": 470,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
8.3835,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
8.3835,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
403,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
86,
|
|
||||||
96.1847,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
138,
|
|
||||||
18.8912,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.7988,
|
|
||||||
138,
|
|
||||||
15.0085,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.8073,
|
|
||||||
138,
|
|
||||||
13.333,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.1403,
|
|
||||||
138,
|
|
||||||
16.5553,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.6956,
|
|
||||||
138,
|
|
||||||
16.5553,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
82.2509,
|
|
||||||
138,
|
|
||||||
15.8415,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
176,
|
|
||||||
18.8912,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.7988,
|
|
||||||
176,
|
|
||||||
15.0085,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.8073,
|
|
||||||
176,
|
|
||||||
13.333,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.1403,
|
|
||||||
176,
|
|
||||||
16.5553,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.6956,
|
|
||||||
176,
|
|
||||||
16.5553,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
82.2509,
|
|
||||||
176,
|
|
||||||
15.8415,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
231,
|
|
||||||
18.8912,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.7988,
|
|
||||||
231,
|
|
||||||
15.0085,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.8073,
|
|
||||||
231,
|
|
||||||
13.333,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.1403,
|
|
||||||
231,
|
|
||||||
16.5553,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.6956,
|
|
||||||
231,
|
|
||||||
16.5553,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
82.2509,
|
|
||||||
231,
|
|
||||||
15.8415,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
286,
|
|
||||||
18.8912,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.7988,
|
|
||||||
286,
|
|
||||||
15.0085,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.8073,
|
|
||||||
286,
|
|
||||||
13.333,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.1403,
|
|
||||||
286,
|
|
||||||
16.5553,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.6956,
|
|
||||||
286,
|
|
||||||
16.5553,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
82.2509,
|
|
||||||
286,
|
|
||||||
15.8415,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
341,
|
|
||||||
18.8912,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.7988,
|
|
||||||
341,
|
|
||||||
15.0085,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.8073,
|
|
||||||
341,
|
|
||||||
13.333,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.1403,
|
|
||||||
341,
|
|
||||||
16.5553,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.6956,
|
|
||||||
341,
|
|
||||||
16.5553,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
82.2509,
|
|
||||||
341,
|
|
||||||
15.8415,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
396,
|
|
||||||
18.8912,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.7988,
|
|
||||||
396,
|
|
||||||
15.0085,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.8073,
|
|
||||||
396,
|
|
||||||
13.333,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.1403,
|
|
||||||
396,
|
|
||||||
16.5553,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.6956,
|
|
||||||
396,
|
|
||||||
16.5553,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
82.2509,
|
|
||||||
396,
|
|
||||||
15.8415,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "62d793eb0343d832087d687b76639e09"
|
|
||||||
}
|
|
||||||
@@ -1,998 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "orders",
|
|
||||||
"viewportWidth": 351,
|
|
||||||
"width": 351,
|
|
||||||
"height": 497,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
26,
|
|
||||||
100,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
61,
|
|
||||||
100,
|
|
||||||
436,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
74,
|
|
||||||
92.5926,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
134,
|
|
||||||
26.7495,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.4532,
|
|
||||||
134,
|
|
||||||
29.1043,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
59.5575,
|
|
||||||
134,
|
|
||||||
20.6019,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
80.1594,
|
|
||||||
134,
|
|
||||||
26.6515,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
106.8109,
|
|
||||||
134,
|
|
||||||
23.0502,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
129.8611,
|
|
||||||
134,
|
|
||||||
21.2028,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
151.0639,
|
|
||||||
134,
|
|
||||||
17.094,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
165,
|
|
||||||
26.7495,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.4532,
|
|
||||||
165,
|
|
||||||
29.1043,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
59.5575,
|
|
||||||
165,
|
|
||||||
20.6019,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
80.1594,
|
|
||||||
165,
|
|
||||||
26.6515,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
106.8109,
|
|
||||||
165,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
129.8611,
|
|
||||||
165,
|
|
||||||
21.2028,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
151.0639,
|
|
||||||
165,
|
|
||||||
17.094,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
226,
|
|
||||||
26.7495,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.4532,
|
|
||||||
226,
|
|
||||||
29.1043,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
59.5575,
|
|
||||||
226,
|
|
||||||
20.6019,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
80.1594,
|
|
||||||
226,
|
|
||||||
26.6515,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
106.8109,
|
|
||||||
226,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
129.8611,
|
|
||||||
226,
|
|
||||||
21.2028,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
151.0639,
|
|
||||||
226,
|
|
||||||
17.094,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
287,
|
|
||||||
26.7495,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.4532,
|
|
||||||
287,
|
|
||||||
29.1043,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
59.5575,
|
|
||||||
287,
|
|
||||||
20.6019,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
80.1594,
|
|
||||||
287,
|
|
||||||
26.6515,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
106.8109,
|
|
||||||
287,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
129.8611,
|
|
||||||
287,
|
|
||||||
21.2028,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
151.0639,
|
|
||||||
287,
|
|
||||||
17.094,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
348,
|
|
||||||
26.7495,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.4532,
|
|
||||||
348,
|
|
||||||
29.1043,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
59.5575,
|
|
||||||
348,
|
|
||||||
20.6019,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
80.1594,
|
|
||||||
348,
|
|
||||||
26.6515,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
106.8109,
|
|
||||||
348,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
129.8611,
|
|
||||||
348,
|
|
||||||
21.2028,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
151.0639,
|
|
||||||
348,
|
|
||||||
17.094,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
409,
|
|
||||||
26.7495,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.4532,
|
|
||||||
409,
|
|
||||||
29.1043,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
59.5575,
|
|
||||||
409,
|
|
||||||
20.6019,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
80.1594,
|
|
||||||
409,
|
|
||||||
26.6515,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
106.8109,
|
|
||||||
409,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
129.8611,
|
|
||||||
409,
|
|
||||||
21.2028,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
151.0639,
|
|
||||||
409,
|
|
||||||
17.094,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "orders",
|
|
||||||
"viewportWidth": 736,
|
|
||||||
"width": 736,
|
|
||||||
"height": 502,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
16.6461,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
16.6461,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
435,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
86,
|
|
||||||
94.837,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
146,
|
|
||||||
15.642,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.2235,
|
|
||||||
146,
|
|
||||||
16.9837,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.2072,
|
|
||||||
146,
|
|
||||||
12.1306,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.3378,
|
|
||||||
146,
|
|
||||||
14.5338,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
61.8716,
|
|
||||||
146,
|
|
||||||
13.5296,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
75.4012,
|
|
||||||
146,
|
|
||||||
12.4745,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.8758,
|
|
||||||
146,
|
|
||||||
9.5427,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
179,
|
|
||||||
15.642,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.2235,
|
|
||||||
179,
|
|
||||||
16.9837,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.2072,
|
|
||||||
179,
|
|
||||||
12.1306,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.3378,
|
|
||||||
179,
|
|
||||||
14.5338,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
61.8716,
|
|
||||||
179,
|
|
||||||
13.5296,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
75.4012,
|
|
||||||
179,
|
|
||||||
12.4745,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.8758,
|
|
||||||
179,
|
|
||||||
9.5427,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
240,
|
|
||||||
15.642,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.2235,
|
|
||||||
240,
|
|
||||||
16.9837,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.2072,
|
|
||||||
240,
|
|
||||||
12.1306,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.3378,
|
|
||||||
240,
|
|
||||||
14.5338,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
61.8716,
|
|
||||||
240,
|
|
||||||
13.5296,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
75.4012,
|
|
||||||
240,
|
|
||||||
12.4745,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.8758,
|
|
||||||
240,
|
|
||||||
9.5427,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
301,
|
|
||||||
15.642,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.2235,
|
|
||||||
301,
|
|
||||||
16.9837,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.2072,
|
|
||||||
301,
|
|
||||||
12.1306,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.3378,
|
|
||||||
301,
|
|
||||||
14.5338,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
61.8716,
|
|
||||||
301,
|
|
||||||
13.5296,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
75.4012,
|
|
||||||
301,
|
|
||||||
12.4745,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.8758,
|
|
||||||
301,
|
|
||||||
9.5427,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
362,
|
|
||||||
15.642,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.2235,
|
|
||||||
362,
|
|
||||||
16.9837,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.2072,
|
|
||||||
362,
|
|
||||||
12.1306,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.3378,
|
|
||||||
362,
|
|
||||||
14.5338,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
61.8716,
|
|
||||||
362,
|
|
||||||
13.5296,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
75.4012,
|
|
||||||
362,
|
|
||||||
12.4745,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.8758,
|
|
||||||
362,
|
|
||||||
9.5427,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
423,
|
|
||||||
15.642,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.2235,
|
|
||||||
423,
|
|
||||||
16.9837,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.2072,
|
|
||||||
423,
|
|
||||||
12.1306,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.3378,
|
|
||||||
423,
|
|
||||||
14.5338,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
61.8716,
|
|
||||||
423,
|
|
||||||
13.5296,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
75.4012,
|
|
||||||
423,
|
|
||||||
12.4745,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.8758,
|
|
||||||
423,
|
|
||||||
9.5427,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "orders",
|
|
||||||
"viewportWidth": 996,
|
|
||||||
"width": 996,
|
|
||||||
"height": 470,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
12.3008,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
12.3008,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
403,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
86,
|
|
||||||
96.1847,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
138,
|
|
||||||
16.2243,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.1319,
|
|
||||||
138,
|
|
||||||
17.5044,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.6363,
|
|
||||||
138,
|
|
||||||
12.8891,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.5254,
|
|
||||||
138,
|
|
||||||
13.7535,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.2788,
|
|
||||||
138,
|
|
||||||
14.2178,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
76.4966,
|
|
||||||
138,
|
|
||||||
13.2138,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
89.7104,
|
|
||||||
138,
|
|
||||||
8.382,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
176,
|
|
||||||
16.2243,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.1319,
|
|
||||||
176,
|
|
||||||
17.5044,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.6363,
|
|
||||||
176,
|
|
||||||
12.8891,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.5254,
|
|
||||||
176,
|
|
||||||
13.7535,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.2788,
|
|
||||||
176,
|
|
||||||
14.2178,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
76.4966,
|
|
||||||
176,
|
|
||||||
13.2138,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
89.7104,
|
|
||||||
176,
|
|
||||||
8.382,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
231,
|
|
||||||
16.2243,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.1319,
|
|
||||||
231,
|
|
||||||
17.5044,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.6363,
|
|
||||||
231,
|
|
||||||
12.8891,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.5254,
|
|
||||||
231,
|
|
||||||
13.7535,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.2788,
|
|
||||||
231,
|
|
||||||
14.2178,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
76.4966,
|
|
||||||
231,
|
|
||||||
13.2138,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
89.7104,
|
|
||||||
231,
|
|
||||||
8.382,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
286,
|
|
||||||
16.2243,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.1319,
|
|
||||||
286,
|
|
||||||
17.5044,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.6363,
|
|
||||||
286,
|
|
||||||
12.8891,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.5254,
|
|
||||||
286,
|
|
||||||
13.7535,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.2788,
|
|
||||||
286,
|
|
||||||
14.2178,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
76.4966,
|
|
||||||
286,
|
|
||||||
13.2138,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
89.7104,
|
|
||||||
286,
|
|
||||||
8.382,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
341,
|
|
||||||
16.2243,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.1319,
|
|
||||||
341,
|
|
||||||
17.5044,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.6363,
|
|
||||||
341,
|
|
||||||
12.8891,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.5254,
|
|
||||||
341,
|
|
||||||
13.7535,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.2788,
|
|
||||||
341,
|
|
||||||
14.2178,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
76.4966,
|
|
||||||
341,
|
|
||||||
13.2138,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
89.7104,
|
|
||||||
341,
|
|
||||||
8.382,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
396,
|
|
||||||
16.2243,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.1319,
|
|
||||||
396,
|
|
||||||
17.5044,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.6363,
|
|
||||||
396,
|
|
||||||
12.8891,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.5254,
|
|
||||||
396,
|
|
||||||
13.7535,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.2788,
|
|
||||||
396,
|
|
||||||
14.2178,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
76.4966,
|
|
||||||
396,
|
|
||||||
13.2138,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
89.7104,
|
|
||||||
396,
|
|
||||||
8.382,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "677a0002aa805c9f7790bc68c6374bb5"
|
|
||||||
}
|
|
||||||
@@ -1,371 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "project-detail",
|
|
||||||
"viewportWidth": 351,
|
|
||||||
"width": 351,
|
|
||||||
"height": 481,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
3.4188,
|
|
||||||
12,
|
|
||||||
5.698,
|
|
||||||
20,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
14.8148,
|
|
||||||
9,
|
|
||||||
50.2315,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
52,
|
|
||||||
48.0057,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.4245,
|
|
||||||
52,
|
|
||||||
48.5755,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
112,
|
|
||||||
100,
|
|
||||||
188,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
125,
|
|
||||||
48.1481,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
154,
|
|
||||||
48.1481,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
56.4103,
|
|
||||||
125,
|
|
||||||
48.1481,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
56.4103,
|
|
||||||
154,
|
|
||||||
48.1481,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
214,
|
|
||||||
48.1481,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
243,
|
|
||||||
48.1481,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
56.4103,
|
|
||||||
214,
|
|
||||||
48.1481,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
56.4103,
|
|
||||||
243,
|
|
||||||
48.1481,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
316,
|
|
||||||
100,
|
|
||||||
165,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
329,
|
|
||||||
92.5926,
|
|
||||||
17,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
357,
|
|
||||||
92.5926,
|
|
||||||
104,
|
|
||||||
8
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "project-detail",
|
|
||||||
"viewportWidth": 736,
|
|
||||||
"width": 736,
|
|
||||||
"height": 453,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
1.6304,
|
|
||||||
12,
|
|
||||||
2.7174,
|
|
||||||
20,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
7.0652,
|
|
||||||
7,
|
|
||||||
29.2799,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
78.2375,
|
|
||||||
0,
|
|
||||||
9.1733,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
89.0413,
|
|
||||||
0,
|
|
||||||
10.9587,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
60,
|
|
||||||
100,
|
|
||||||
200,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
79,
|
|
||||||
46.3315,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
108,
|
|
||||||
46.3315,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.087,
|
|
||||||
79,
|
|
||||||
46.3315,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.087,
|
|
||||||
108,
|
|
||||||
46.3315,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
168,
|
|
||||||
46.3315,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
197,
|
|
||||||
46.3315,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.087,
|
|
||||||
168,
|
|
||||||
46.3315,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.087,
|
|
||||||
197,
|
|
||||||
46.3315,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
276,
|
|
||||||
100,
|
|
||||||
177,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
295,
|
|
||||||
94.837,
|
|
||||||
17,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
323,
|
|
||||||
94.837,
|
|
||||||
104,
|
|
||||||
8
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "project-detail",
|
|
||||||
"viewportWidth": 996,
|
|
||||||
"width": 996,
|
|
||||||
"height": 404,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0.6024,
|
|
||||||
7,
|
|
||||||
2.008,
|
|
||||||
20,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.0161,
|
|
||||||
2,
|
|
||||||
21.6365,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
84.7217,
|
|
||||||
0,
|
|
||||||
6.3771,
|
|
||||||
34,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
92.3036,
|
|
||||||
0,
|
|
||||||
7.6964,
|
|
||||||
34,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
50,
|
|
||||||
100,
|
|
||||||
180,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
69,
|
|
||||||
47.2892,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
96,
|
|
||||||
47.2892,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
50.8032,
|
|
||||||
69,
|
|
||||||
47.2892,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
50.8032,
|
|
||||||
96,
|
|
||||||
47.2892,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
148,
|
|
||||||
47.2892,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
175,
|
|
||||||
47.2892,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
50.8032,
|
|
||||||
148,
|
|
||||||
47.2892,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
50.8032,
|
|
||||||
175,
|
|
||||||
47.2892,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
246,
|
|
||||||
100,
|
|
||||||
157,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
265,
|
|
||||||
96.1847,
|
|
||||||
17,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
294,
|
|
||||||
96.1847,
|
|
||||||
84,
|
|
||||||
8
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "ab5e1f108d42c55b0e6382fcaffff793"
|
|
||||||
}
|
|
||||||
@@ -1,746 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "projects",
|
|
||||||
"viewportWidth": 351,
|
|
||||||
"width": 351,
|
|
||||||
"height": 497,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
26,
|
|
||||||
100,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
61,
|
|
||||||
100,
|
|
||||||
436,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
74,
|
|
||||||
92.5926,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
134,
|
|
||||||
34.6065,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.3102,
|
|
||||||
134,
|
|
||||||
31.3568,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.667,
|
|
||||||
134,
|
|
||||||
26.6515,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
96.3186,
|
|
||||||
134,
|
|
||||||
27.7066,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
124.0251,
|
|
||||||
134,
|
|
||||||
22.1287,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
165,
|
|
||||||
34.6065,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.3102,
|
|
||||||
165,
|
|
||||||
31.3568,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.667,
|
|
||||||
165,
|
|
||||||
26.6515,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
96.3186,
|
|
||||||
165,
|
|
||||||
27.7066,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
124.0251,
|
|
||||||
165,
|
|
||||||
22.1287,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
226,
|
|
||||||
34.6065,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.3102,
|
|
||||||
226,
|
|
||||||
31.3568,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.667,
|
|
||||||
226,
|
|
||||||
26.6515,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
96.3186,
|
|
||||||
226,
|
|
||||||
27.7066,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
124.0251,
|
|
||||||
226,
|
|
||||||
22.1287,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
287,
|
|
||||||
34.6065,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.3102,
|
|
||||||
287,
|
|
||||||
31.3568,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.667,
|
|
||||||
287,
|
|
||||||
26.6515,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
96.3186,
|
|
||||||
287,
|
|
||||||
27.7066,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
124.0251,
|
|
||||||
287,
|
|
||||||
22.1287,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
348,
|
|
||||||
34.6065,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.3102,
|
|
||||||
348,
|
|
||||||
31.3568,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.667,
|
|
||||||
348,
|
|
||||||
26.6515,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
96.3186,
|
|
||||||
348,
|
|
||||||
27.7066,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
124.0251,
|
|
||||||
348,
|
|
||||||
22.1287,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
409,
|
|
||||||
34.6065,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.3102,
|
|
||||||
409,
|
|
||||||
31.3568,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.667,
|
|
||||||
409,
|
|
||||||
26.6515,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
96.3186,
|
|
||||||
409,
|
|
||||||
27.7066,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
124.0251,
|
|
||||||
409,
|
|
||||||
22.1287,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "projects",
|
|
||||||
"viewportWidth": 736,
|
|
||||||
"width": 736,
|
|
||||||
"height": 502,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
10.9099,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
10.9099,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
435,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
86,
|
|
||||||
94.837,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
146,
|
|
||||||
23.429,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.0105,
|
|
||||||
146,
|
|
||||||
21.2806,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.2911,
|
|
||||||
146,
|
|
||||||
18.1704,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.4615,
|
|
||||||
146,
|
|
||||||
17.6694,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
83.1309,
|
|
||||||
146,
|
|
||||||
14.2875,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
179,
|
|
||||||
23.429,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.0105,
|
|
||||||
179,
|
|
||||||
21.2806,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.2911,
|
|
||||||
179,
|
|
||||||
18.1704,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.4615,
|
|
||||||
179,
|
|
||||||
17.6694,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
83.1309,
|
|
||||||
179,
|
|
||||||
14.2875,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
240,
|
|
||||||
23.429,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.0105,
|
|
||||||
240,
|
|
||||||
21.2806,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.2911,
|
|
||||||
240,
|
|
||||||
18.1704,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.4615,
|
|
||||||
240,
|
|
||||||
17.6694,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
83.1309,
|
|
||||||
240,
|
|
||||||
14.2875,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
301,
|
|
||||||
23.429,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.0105,
|
|
||||||
301,
|
|
||||||
21.2806,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.2911,
|
|
||||||
301,
|
|
||||||
18.1704,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.4615,
|
|
||||||
301,
|
|
||||||
17.6694,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
83.1309,
|
|
||||||
301,
|
|
||||||
14.2875,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
362,
|
|
||||||
23.429,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.0105,
|
|
||||||
362,
|
|
||||||
21.2806,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.2911,
|
|
||||||
362,
|
|
||||||
18.1704,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.4615,
|
|
||||||
362,
|
|
||||||
17.6694,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
83.1309,
|
|
||||||
362,
|
|
||||||
14.2875,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
423,
|
|
||||||
23.429,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.0105,
|
|
||||||
423,
|
|
||||||
21.2806,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.2911,
|
|
||||||
423,
|
|
||||||
18.1704,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.4615,
|
|
||||||
423,
|
|
||||||
17.6694,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
83.1309,
|
|
||||||
423,
|
|
||||||
14.2875,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "projects",
|
|
||||||
"viewportWidth": 996,
|
|
||||||
"width": 996,
|
|
||||||
"height": 470,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
8.0619,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
8.0619,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
403,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
86,
|
|
||||||
96.1847,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
138,
|
|
||||||
24.4588,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.3664,
|
|
||||||
138,
|
|
||||||
22.4068,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.7732,
|
|
||||||
138,
|
|
||||||
19.4308,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
68.2041,
|
|
||||||
138,
|
|
||||||
17.2612,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4653,
|
|
||||||
138,
|
|
||||||
12.6271,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
176,
|
|
||||||
24.4588,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.3664,
|
|
||||||
176,
|
|
||||||
22.4068,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.7732,
|
|
||||||
176,
|
|
||||||
19.4308,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
68.2041,
|
|
||||||
176,
|
|
||||||
17.2612,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4653,
|
|
||||||
176,
|
|
||||||
12.6271,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
231,
|
|
||||||
24.4588,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.3664,
|
|
||||||
231,
|
|
||||||
22.4068,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.7732,
|
|
||||||
231,
|
|
||||||
19.4308,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
68.2041,
|
|
||||||
231,
|
|
||||||
17.2612,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4653,
|
|
||||||
231,
|
|
||||||
12.6271,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
286,
|
|
||||||
24.4588,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.3664,
|
|
||||||
286,
|
|
||||||
22.4068,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.7732,
|
|
||||||
286,
|
|
||||||
19.4308,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
68.2041,
|
|
||||||
286,
|
|
||||||
17.2612,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4653,
|
|
||||||
286,
|
|
||||||
12.6271,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
341,
|
|
||||||
24.4588,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.3664,
|
|
||||||
341,
|
|
||||||
22.4068,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.7732,
|
|
||||||
341,
|
|
||||||
19.4308,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
68.2041,
|
|
||||||
341,
|
|
||||||
17.2612,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4653,
|
|
||||||
341,
|
|
||||||
12.6271,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
396,
|
|
||||||
24.4588,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.3664,
|
|
||||||
396,
|
|
||||||
22.4068,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.7732,
|
|
||||||
396,
|
|
||||||
19.4308,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
68.2041,
|
|
||||||
396,
|
|
||||||
17.2612,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4653,
|
|
||||||
396,
|
|
||||||
12.6271,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "17f8285c3ca514ddef6d48c1183ed642"
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
"use client"
|
|
||||||
// Auto-generated by `npx boneyard-js build` — do not edit
|
|
||||||
import { registerBones } from 'boneyard-js'
|
|
||||||
import { configureBoneyard } from 'boneyard-js/react'
|
|
||||||
|
|
||||||
import _dash_sessions from './dash-sessions.bones.json'
|
|
||||||
import _attendance_history_fund from './attendance-history-fund.bones.json'
|
|
||||||
import _attendance_history_table from './attendance-history-table.bones.json'
|
|
||||||
import _leave_requests from './leave-requests.bones.json'
|
|
||||||
import _leave_approval from './leave-approval.bones.json'
|
|
||||||
import _attendance_balances from './attendance-balances.bones.json'
|
|
||||||
import _trips_history from './trips-history.bones.json'
|
|
||||||
import _trips_admin from './trips-admin.bones.json'
|
|
||||||
import _vehicles from './vehicles.bones.json'
|
|
||||||
import _offers from './offers.bones.json'
|
|
||||||
import _orders from './orders.bones.json'
|
|
||||||
import _projects from './projects.bones.json'
|
|
||||||
import _offers_customers from './offers-customers.bones.json'
|
|
||||||
import _users from './users.bones.json'
|
|
||||||
import _audit_log_rows from './audit-log-rows.bones.json'
|
|
||||||
import _offer_detail from './offer-detail.bones.json'
|
|
||||||
import _invoice_detail from './invoice-detail.bones.json'
|
|
||||||
import _project_detail from './project-detail.bones.json'
|
|
||||||
import _attendance_create from './attendance-create.bones.json'
|
|
||||||
import _offers_templates from './offers-templates.bones.json'
|
|
||||||
|
|
||||||
configureBoneyard({"color":"#e0e0e0","animate":"shimmer","shimmerColor":"#f0f0f0","speed":"1.2s","shimmerAngle":110})
|
|
||||||
|
|
||||||
registerBones({
|
|
||||||
"dash-sessions": _dash_sessions,
|
|
||||||
"attendance-history-fund": _attendance_history_fund,
|
|
||||||
"attendance-history-table": _attendance_history_table,
|
|
||||||
"leave-requests": _leave_requests,
|
|
||||||
"leave-approval": _leave_approval,
|
|
||||||
"attendance-balances": _attendance_balances,
|
|
||||||
"trips-history": _trips_history,
|
|
||||||
"trips-admin": _trips_admin,
|
|
||||||
"vehicles": _vehicles,
|
|
||||||
"offers": _offers,
|
|
||||||
"orders": _orders,
|
|
||||||
"projects": _projects,
|
|
||||||
"offers-customers": _offers_customers,
|
|
||||||
"users": _users,
|
|
||||||
"audit-log-rows": _audit_log_rows,
|
|
||||||
"offer-detail": _offer_detail,
|
|
||||||
"invoice-detail": _invoice_detail,
|
|
||||||
"project-detail": _project_detail,
|
|
||||||
"attendance-create": _attendance_create,
|
|
||||||
"offers-templates": _offers_templates,
|
|
||||||
})
|
|
||||||
@@ -1,725 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "trips-admin",
|
|
||||||
"viewportWidth": 317,
|
|
||||||
"width": 317,
|
|
||||||
"height": 437,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
26,
|
|
||||||
100,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
61,
|
|
||||||
100,
|
|
||||||
376,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.1009,
|
|
||||||
74,
|
|
||||||
37.8795,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.9805,
|
|
||||||
74,
|
|
||||||
43.4592,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4397,
|
|
||||||
74,
|
|
||||||
31.6739,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
117.1136,
|
|
||||||
74,
|
|
||||||
16.6108,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
133.7244,
|
|
||||||
74,
|
|
||||||
28.1053,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.1009,
|
|
||||||
105,
|
|
||||||
37.8795,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.9805,
|
|
||||||
105,
|
|
||||||
43.4592,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4397,
|
|
||||||
105,
|
|
||||||
31.6739,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
117.1136,
|
|
||||||
105,
|
|
||||||
16.6108,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
133.7244,
|
|
||||||
105,
|
|
||||||
28.1053,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.1009,
|
|
||||||
166,
|
|
||||||
37.8795,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.9805,
|
|
||||||
166,
|
|
||||||
43.4592,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4397,
|
|
||||||
166,
|
|
||||||
31.6739,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
117.1136,
|
|
||||||
166,
|
|
||||||
16.6108,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
133.7244,
|
|
||||||
166,
|
|
||||||
28.1053,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.1009,
|
|
||||||
227,
|
|
||||||
37.8795,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.9805,
|
|
||||||
227,
|
|
||||||
43.4592,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4397,
|
|
||||||
227,
|
|
||||||
31.6739,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
117.1136,
|
|
||||||
227,
|
|
||||||
16.6108,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
133.7244,
|
|
||||||
227,
|
|
||||||
28.1053,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.1009,
|
|
||||||
288,
|
|
||||||
37.8795,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.9805,
|
|
||||||
288,
|
|
||||||
43.4592,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4397,
|
|
||||||
288,
|
|
||||||
31.6739,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
117.1136,
|
|
||||||
288,
|
|
||||||
16.6108,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
133.7244,
|
|
||||||
288,
|
|
||||||
28.1053,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.1009,
|
|
||||||
349,
|
|
||||||
37.8795,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.9805,
|
|
||||||
349,
|
|
||||||
43.4592,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4397,
|
|
||||||
349,
|
|
||||||
31.6739,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
117.1136,
|
|
||||||
349,
|
|
||||||
16.6108,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
133.7244,
|
|
||||||
349,
|
|
||||||
28.1053,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "trips-admin",
|
|
||||||
"viewportWidth": 690,
|
|
||||||
"width": 690,
|
|
||||||
"height": 442,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
16.3202,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
16.3202,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
375,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.7536,
|
|
||||||
86,
|
|
||||||
22.8057,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.5593,
|
|
||||||
86,
|
|
||||||
26.0711,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.6304,
|
|
||||||
86,
|
|
||||||
19.1757,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.8062,
|
|
||||||
86,
|
|
||||||
10.3601,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.1662,
|
|
||||||
86,
|
|
||||||
16.0802,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.7536,
|
|
||||||
119,
|
|
||||||
22.8057,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.5593,
|
|
||||||
119,
|
|
||||||
26.0711,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.6304,
|
|
||||||
119,
|
|
||||||
19.1757,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.8062,
|
|
||||||
119,
|
|
||||||
10.3601,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.1662,
|
|
||||||
119,
|
|
||||||
16.0802,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.7536,
|
|
||||||
180,
|
|
||||||
22.8057,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.5593,
|
|
||||||
180,
|
|
||||||
26.0711,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.6304,
|
|
||||||
180,
|
|
||||||
19.1757,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.8062,
|
|
||||||
180,
|
|
||||||
10.3601,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.1662,
|
|
||||||
180,
|
|
||||||
16.0802,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.7536,
|
|
||||||
241,
|
|
||||||
22.8057,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.5593,
|
|
||||||
241,
|
|
||||||
26.0711,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.6304,
|
|
||||||
241,
|
|
||||||
19.1757,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.8062,
|
|
||||||
241,
|
|
||||||
10.3601,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.1662,
|
|
||||||
241,
|
|
||||||
16.0802,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.7536,
|
|
||||||
302,
|
|
||||||
22.8057,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.5593,
|
|
||||||
302,
|
|
||||||
26.0711,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.6304,
|
|
||||||
302,
|
|
||||||
19.1757,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.8062,
|
|
||||||
302,
|
|
||||||
10.3601,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.1662,
|
|
||||||
302,
|
|
||||||
16.0802,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.7536,
|
|
||||||
363,
|
|
||||||
22.8057,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.5593,
|
|
||||||
363,
|
|
||||||
26.0711,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.6304,
|
|
||||||
363,
|
|
||||||
19.1757,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.8062,
|
|
||||||
363,
|
|
||||||
10.3601,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.1662,
|
|
||||||
363,
|
|
||||||
16.0802,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "trips-admin",
|
|
||||||
"viewportWidth": 950,
|
|
||||||
"width": 950,
|
|
||||||
"height": 418,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
11.8536,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
11.8536,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
351,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2,
|
|
||||||
86,
|
|
||||||
23.523,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.523,
|
|
||||||
86,
|
|
||||||
26.574,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.097,
|
|
||||||
86,
|
|
||||||
20.1382,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.2352,
|
|
||||||
86,
|
|
||||||
11.9046,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
84.1398,
|
|
||||||
86,
|
|
||||||
13.8602,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2,
|
|
||||||
124,
|
|
||||||
23.523,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.523,
|
|
||||||
124,
|
|
||||||
26.574,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.097,
|
|
||||||
124,
|
|
||||||
20.1382,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.2352,
|
|
||||||
124,
|
|
||||||
11.9046,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
84.1398,
|
|
||||||
124,
|
|
||||||
13.8602,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2,
|
|
||||||
179,
|
|
||||||
23.523,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.523,
|
|
||||||
179,
|
|
||||||
26.574,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.097,
|
|
||||||
179,
|
|
||||||
20.1382,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.2352,
|
|
||||||
179,
|
|
||||||
11.9046,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
84.1398,
|
|
||||||
179,
|
|
||||||
13.8602,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2,
|
|
||||||
234,
|
|
||||||
23.523,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.523,
|
|
||||||
234,
|
|
||||||
26.574,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.097,
|
|
||||||
234,
|
|
||||||
20.1382,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.2352,
|
|
||||||
234,
|
|
||||||
11.9046,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
84.1398,
|
|
||||||
234,
|
|
||||||
13.8602,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2,
|
|
||||||
289,
|
|
||||||
23.523,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.523,
|
|
||||||
289,
|
|
||||||
26.574,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.097,
|
|
||||||
289,
|
|
||||||
20.1382,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.2352,
|
|
||||||
289,
|
|
||||||
11.9046,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
84.1398,
|
|
||||||
289,
|
|
||||||
13.8602,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2,
|
|
||||||
344,
|
|
||||||
23.523,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.523,
|
|
||||||
344,
|
|
||||||
26.574,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.097,
|
|
||||||
344,
|
|
||||||
20.1382,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.2352,
|
|
||||||
344,
|
|
||||||
11.9046,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
84.1398,
|
|
||||||
344,
|
|
||||||
13.8602,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "39a325f430c84bb51960a684759a8f0c"
|
|
||||||
}
|
|
||||||
@@ -1,725 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "trips-history",
|
|
||||||
"viewportWidth": 317,
|
|
||||||
"width": 317,
|
|
||||||
"height": 300,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
26,
|
|
||||||
100,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
61,
|
|
||||||
100,
|
|
||||||
239,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.1009,
|
|
||||||
74,
|
|
||||||
35.2326,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
39.3336,
|
|
||||||
74,
|
|
||||||
40.4278,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
79.7614,
|
|
||||||
74,
|
|
||||||
29.4657,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.2271,
|
|
||||||
74,
|
|
||||||
37.1402,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
146.3673,
|
|
||||||
74,
|
|
||||||
15.4623,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.1009,
|
|
||||||
105,
|
|
||||||
35.2326,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
39.3336,
|
|
||||||
105,
|
|
||||||
40.4278,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
79.7614,
|
|
||||||
105,
|
|
||||||
29.4657,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.2271,
|
|
||||||
105,
|
|
||||||
37.1402,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
146.3673,
|
|
||||||
105,
|
|
||||||
15.4623,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.1009,
|
|
||||||
138,
|
|
||||||
35.2326,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
39.3336,
|
|
||||||
138,
|
|
||||||
40.4278,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
79.7614,
|
|
||||||
138,
|
|
||||||
29.4657,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.2271,
|
|
||||||
138,
|
|
||||||
37.1402,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
146.3673,
|
|
||||||
138,
|
|
||||||
15.4623,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.1009,
|
|
||||||
172,
|
|
||||||
35.2326,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
39.3336,
|
|
||||||
172,
|
|
||||||
40.4278,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
79.7614,
|
|
||||||
172,
|
|
||||||
29.4657,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.2271,
|
|
||||||
172,
|
|
||||||
37.1402,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
146.3673,
|
|
||||||
172,
|
|
||||||
15.4623,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.1009,
|
|
||||||
205,
|
|
||||||
35.2326,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
39.3336,
|
|
||||||
205,
|
|
||||||
40.4278,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
79.7614,
|
|
||||||
205,
|
|
||||||
29.4657,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.2271,
|
|
||||||
205,
|
|
||||||
37.1402,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
146.3673,
|
|
||||||
205,
|
|
||||||
15.4623,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.1009,
|
|
||||||
239,
|
|
||||||
35.2326,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
39.3336,
|
|
||||||
239,
|
|
||||||
40.4278,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
79.7614,
|
|
||||||
239,
|
|
||||||
29.4657,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.2271,
|
|
||||||
239,
|
|
||||||
37.1402,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
146.3673,
|
|
||||||
239,
|
|
||||||
15.4623,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "trips-history",
|
|
||||||
"viewportWidth": 690,
|
|
||||||
"width": 690,
|
|
||||||
"height": 312,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
16.6033,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
16.6033,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
245,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.7536,
|
|
||||||
86,
|
|
||||||
21.0417,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.7953,
|
|
||||||
86,
|
|
||||||
24.0534,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.8487,
|
|
||||||
86,
|
|
||||||
17.6925,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.5412,
|
|
||||||
86,
|
|
||||||
22.1445,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.6857,
|
|
||||||
86,
|
|
||||||
9.5607,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.7536,
|
|
||||||
119,
|
|
||||||
21.0417,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.7953,
|
|
||||||
119,
|
|
||||||
24.0534,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.8487,
|
|
||||||
119,
|
|
||||||
17.6925,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.5412,
|
|
||||||
119,
|
|
||||||
22.1445,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.6857,
|
|
||||||
119,
|
|
||||||
9.5607,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.7536,
|
|
||||||
154,
|
|
||||||
21.0417,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.7953,
|
|
||||||
154,
|
|
||||||
24.0534,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.8487,
|
|
||||||
154,
|
|
||||||
17.6925,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.5412,
|
|
||||||
154,
|
|
||||||
22.1445,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.6857,
|
|
||||||
154,
|
|
||||||
9.5607,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.7536,
|
|
||||||
189,
|
|
||||||
21.0417,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.7953,
|
|
||||||
189,
|
|
||||||
24.0534,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.8487,
|
|
||||||
189,
|
|
||||||
17.6925,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.5412,
|
|
||||||
189,
|
|
||||||
22.1445,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.6857,
|
|
||||||
189,
|
|
||||||
9.5607,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.7536,
|
|
||||||
224,
|
|
||||||
21.0417,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.7953,
|
|
||||||
224,
|
|
||||||
24.0534,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.8487,
|
|
||||||
224,
|
|
||||||
17.6925,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.5412,
|
|
||||||
224,
|
|
||||||
22.1445,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.6857,
|
|
||||||
224,
|
|
||||||
9.5607,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.7536,
|
|
||||||
259,
|
|
||||||
21.0417,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.7953,
|
|
||||||
259,
|
|
||||||
24.0534,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.8487,
|
|
||||||
259,
|
|
||||||
17.6925,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.5412,
|
|
||||||
259,
|
|
||||||
22.1445,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.6857,
|
|
||||||
259,
|
|
||||||
9.5607,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "trips-history",
|
|
||||||
"viewportWidth": 958,
|
|
||||||
"width": 958,
|
|
||||||
"height": 355,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
11.9585,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
11.9585,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
288,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9833,
|
|
||||||
86,
|
|
||||||
21.1541,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.1374,
|
|
||||||
86,
|
|
||||||
23.8974,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.0348,
|
|
||||||
86,
|
|
||||||
18.1106,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.1455,
|
|
||||||
86,
|
|
||||||
22.1604,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.3059,
|
|
||||||
86,
|
|
||||||
10.7108,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9833,
|
|
||||||
124,
|
|
||||||
21.1541,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.1374,
|
|
||||||
124,
|
|
||||||
23.8974,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.0348,
|
|
||||||
124,
|
|
||||||
18.1106,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.1455,
|
|
||||||
124,
|
|
||||||
22.1604,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.3059,
|
|
||||||
124,
|
|
||||||
10.7108,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9833,
|
|
||||||
167,
|
|
||||||
21.1541,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.1374,
|
|
||||||
167,
|
|
||||||
23.8974,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.0348,
|
|
||||||
167,
|
|
||||||
18.1106,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.1455,
|
|
||||||
167,
|
|
||||||
22.1604,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.3059,
|
|
||||||
167,
|
|
||||||
10.7108,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9833,
|
|
||||||
209,
|
|
||||||
21.1541,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.1374,
|
|
||||||
209,
|
|
||||||
23.8974,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.0348,
|
|
||||||
209,
|
|
||||||
18.1106,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.1455,
|
|
||||||
209,
|
|
||||||
22.1604,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.3059,
|
|
||||||
209,
|
|
||||||
10.7108,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9833,
|
|
||||||
252,
|
|
||||||
21.1541,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.1374,
|
|
||||||
252,
|
|
||||||
23.8974,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.0348,
|
|
||||||
252,
|
|
||||||
18.1106,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.1455,
|
|
||||||
252,
|
|
||||||
22.1604,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.3059,
|
|
||||||
252,
|
|
||||||
10.7108,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9833,
|
|
||||||
294,
|
|
||||||
21.1541,
|
|
||||||
42,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.1374,
|
|
||||||
294,
|
|
||||||
23.8974,
|
|
||||||
42,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.0348,
|
|
||||||
294,
|
|
||||||
18.1106,
|
|
||||||
42,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.1455,
|
|
||||||
294,
|
|
||||||
22.1604,
|
|
||||||
42,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.3059,
|
|
||||||
294,
|
|
||||||
10.7108,
|
|
||||||
42,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "6b54a0afbb4863895e318916b1fdca67"
|
|
||||||
}
|
|
||||||
@@ -1,767 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "users",
|
|
||||||
"viewportWidth": 351,
|
|
||||||
"width": 351,
|
|
||||||
"height": 549,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
26,
|
|
||||||
100,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
53,
|
|
||||||
100,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
113,
|
|
||||||
100,
|
|
||||||
436,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
126,
|
|
||||||
92.5926,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
186,
|
|
||||||
37.362,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.0657,
|
|
||||||
186,
|
|
||||||
26.429,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
67.4947,
|
|
||||||
186,
|
|
||||||
36.1779,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
103.6725,
|
|
||||||
186,
|
|
||||||
23.62,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
127.2926,
|
|
||||||
186,
|
|
||||||
18.8613,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
217,
|
|
||||||
37.362,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.0657,
|
|
||||||
217,
|
|
||||||
26.429,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
67.4947,
|
|
||||||
217,
|
|
||||||
36.1779,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
103.6725,
|
|
||||||
217,
|
|
||||||
23.62,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
127.2926,
|
|
||||||
217,
|
|
||||||
18.8613,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
278,
|
|
||||||
37.362,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.0657,
|
|
||||||
278,
|
|
||||||
26.429,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
67.4947,
|
|
||||||
278,
|
|
||||||
36.1779,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
103.6725,
|
|
||||||
278,
|
|
||||||
23.62,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
127.2926,
|
|
||||||
278,
|
|
||||||
18.8613,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
339,
|
|
||||||
37.362,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.0657,
|
|
||||||
339,
|
|
||||||
26.429,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
67.4947,
|
|
||||||
339,
|
|
||||||
36.1779,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
103.6725,
|
|
||||||
339,
|
|
||||||
23.62,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
127.2926,
|
|
||||||
339,
|
|
||||||
18.8613,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
400,
|
|
||||||
37.362,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.0657,
|
|
||||||
400,
|
|
||||||
26.429,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
67.4947,
|
|
||||||
400,
|
|
||||||
36.1779,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
103.6725,
|
|
||||||
400,
|
|
||||||
23.62,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
127.2926,
|
|
||||||
400,
|
|
||||||
18.8613,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
461,
|
|
||||||
37.362,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.0657,
|
|
||||||
461,
|
|
||||||
26.429,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
67.4947,
|
|
||||||
461,
|
|
||||||
36.1779,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
103.6725,
|
|
||||||
461,
|
|
||||||
23.62,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
127.2926,
|
|
||||||
461,
|
|
||||||
18.8613,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "users",
|
|
||||||
"viewportWidth": 736,
|
|
||||||
"width": 736,
|
|
||||||
"height": 502,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
12.6741,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
12.6741,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.2479,
|
|
||||||
4,
|
|
||||||
18.7521,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
435,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
86,
|
|
||||||
94.837,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
146,
|
|
||||||
24.3079,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.8894,
|
|
||||||
146,
|
|
||||||
18.6481,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.5375,
|
|
||||||
146,
|
|
||||||
23.5628,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.1003,
|
|
||||||
146,
|
|
||||||
15.6568,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
84.7571,
|
|
||||||
146,
|
|
||||||
12.6613,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
179,
|
|
||||||
24.3079,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.8894,
|
|
||||||
179,
|
|
||||||
18.6481,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.5375,
|
|
||||||
179,
|
|
||||||
23.5628,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.1003,
|
|
||||||
179,
|
|
||||||
15.6568,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
84.7571,
|
|
||||||
179,
|
|
||||||
12.6613,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
240,
|
|
||||||
24.3079,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.8894,
|
|
||||||
240,
|
|
||||||
18.6481,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.5375,
|
|
||||||
240,
|
|
||||||
23.5628,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.1003,
|
|
||||||
240,
|
|
||||||
15.6568,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
84.7571,
|
|
||||||
240,
|
|
||||||
12.6613,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
301,
|
|
||||||
24.3079,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.8894,
|
|
||||||
301,
|
|
||||||
18.6481,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.5375,
|
|
||||||
301,
|
|
||||||
23.5628,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.1003,
|
|
||||||
301,
|
|
||||||
15.6568,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
84.7571,
|
|
||||||
301,
|
|
||||||
12.6613,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
362,
|
|
||||||
24.3079,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.8894,
|
|
||||||
362,
|
|
||||||
18.6481,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.5375,
|
|
||||||
362,
|
|
||||||
23.5628,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.1003,
|
|
||||||
362,
|
|
||||||
15.6568,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
84.7571,
|
|
||||||
362,
|
|
||||||
12.6613,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
423,
|
|
||||||
24.3079,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.8894,
|
|
||||||
423,
|
|
||||||
18.6481,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.5375,
|
|
||||||
423,
|
|
||||||
23.5628,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.1003,
|
|
||||||
423,
|
|
||||||
15.6568,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
84.7571,
|
|
||||||
423,
|
|
||||||
12.6613,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "users",
|
|
||||||
"viewportWidth": 996,
|
|
||||||
"width": 996,
|
|
||||||
"height": 505,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
9.3656,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
9.3656,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
86.5446,
|
|
||||||
10,
|
|
||||||
13.4554,
|
|
||||||
32,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
438,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
86,
|
|
||||||
96.1847,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
138,
|
|
||||||
25.3655,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.2732,
|
|
||||||
138,
|
|
||||||
20.4302,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.7033,
|
|
||||||
138,
|
|
||||||
22.8571,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.5604,
|
|
||||||
138,
|
|
||||||
15.9011,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
86.4615,
|
|
||||||
138,
|
|
||||||
11.6309,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
176,
|
|
||||||
25.3655,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.2732,
|
|
||||||
176,
|
|
||||||
20.4302,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.7033,
|
|
||||||
176,
|
|
||||||
22.8571,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.5604,
|
|
||||||
176,
|
|
||||||
15.9011,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
86.4615,
|
|
||||||
176,
|
|
||||||
11.6309,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
238,
|
|
||||||
25.3655,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.2732,
|
|
||||||
238,
|
|
||||||
20.4302,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.7033,
|
|
||||||
238,
|
|
||||||
22.8571,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.5604,
|
|
||||||
238,
|
|
||||||
15.9011,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
86.4615,
|
|
||||||
238,
|
|
||||||
11.6309,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
300,
|
|
||||||
25.3655,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.2732,
|
|
||||||
300,
|
|
||||||
20.4302,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.7033,
|
|
||||||
300,
|
|
||||||
22.8571,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.5604,
|
|
||||||
300,
|
|
||||||
15.9011,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
86.4615,
|
|
||||||
300,
|
|
||||||
11.6309,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
362,
|
|
||||||
25.3655,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.2732,
|
|
||||||
362,
|
|
||||||
20.4302,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.7033,
|
|
||||||
362,
|
|
||||||
22.8571,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.5604,
|
|
||||||
362,
|
|
||||||
15.9011,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
86.4615,
|
|
||||||
362,
|
|
||||||
11.6309,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
424,
|
|
||||||
25.3655,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.2732,
|
|
||||||
424,
|
|
||||||
20.4302,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.7033,
|
|
||||||
424,
|
|
||||||
22.8571,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.5604,
|
|
||||||
424,
|
|
||||||
15.9011,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
86.4615,
|
|
||||||
424,
|
|
||||||
11.6309,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "53e8df6c8f8bf975b3b88bfca3bbd804"
|
|
||||||
}
|
|
||||||
@@ -1,746 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "vehicles",
|
|
||||||
"viewportWidth": 351,
|
|
||||||
"width": 351,
|
|
||||||
"height": 530,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
34,
|
|
||||||
100,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
94,
|
|
||||||
100,
|
|
||||||
436,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
107,
|
|
||||||
92.5926,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
167,
|
|
||||||
23.9583,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.662,
|
|
||||||
167,
|
|
||||||
24.4168,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.0789,
|
|
||||||
167,
|
|
||||||
29.042,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.1209,
|
|
||||||
167,
|
|
||||||
18.8435,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
99.9644,
|
|
||||||
167,
|
|
||||||
46.1895,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
197,
|
|
||||||
23.9583,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.662,
|
|
||||||
197,
|
|
||||||
24.4168,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.0789,
|
|
||||||
197,
|
|
||||||
29.042,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.1209,
|
|
||||||
197,
|
|
||||||
18.8435,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
99.9644,
|
|
||||||
197,
|
|
||||||
46.1895,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
258,
|
|
||||||
23.9583,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.662,
|
|
||||||
258,
|
|
||||||
24.4168,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.0789,
|
|
||||||
258,
|
|
||||||
29.042,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.1209,
|
|
||||||
258,
|
|
||||||
18.8435,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
99.9644,
|
|
||||||
258,
|
|
||||||
46.1895,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
319,
|
|
||||||
23.9583,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.662,
|
|
||||||
319,
|
|
||||||
24.4168,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.0789,
|
|
||||||
319,
|
|
||||||
29.042,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.1209,
|
|
||||||
319,
|
|
||||||
18.8435,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
99.9644,
|
|
||||||
319,
|
|
||||||
46.1895,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
380,
|
|
||||||
23.9583,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.662,
|
|
||||||
380,
|
|
||||||
24.4168,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.0789,
|
|
||||||
380,
|
|
||||||
29.042,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.1209,
|
|
||||||
380,
|
|
||||||
18.8435,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
99.9644,
|
|
||||||
380,
|
|
||||||
46.1895,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
441,
|
|
||||||
23.9583,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.662,
|
|
||||||
441,
|
|
||||||
24.4168,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.0789,
|
|
||||||
441,
|
|
||||||
29.042,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.1209,
|
|
||||||
441,
|
|
||||||
18.8435,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
99.9644,
|
|
||||||
441,
|
|
||||||
46.1895,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "vehicles",
|
|
||||||
"viewportWidth": 736,
|
|
||||||
"width": 736,
|
|
||||||
"height": 495,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
7,
|
|
||||||
10.1478,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
82.6427,
|
|
||||||
0,
|
|
||||||
17.3573,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
60,
|
|
||||||
100,
|
|
||||||
435,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
79,
|
|
||||||
94.837,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
139,
|
|
||||||
16.4126,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.9941,
|
|
||||||
139,
|
|
||||||
16.5039,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.498,
|
|
||||||
139,
|
|
||||||
19.5058,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
55.0038,
|
|
||||||
139,
|
|
||||||
12.8843,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
67.8881,
|
|
||||||
139,
|
|
||||||
29.5304,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
172,
|
|
||||||
16.4126,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.9941,
|
|
||||||
172,
|
|
||||||
16.5039,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.498,
|
|
||||||
172,
|
|
||||||
19.5058,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
55.0038,
|
|
||||||
172,
|
|
||||||
12.8843,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
67.8881,
|
|
||||||
172,
|
|
||||||
29.5304,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
233,
|
|
||||||
16.4126,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.9941,
|
|
||||||
233,
|
|
||||||
16.5039,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.498,
|
|
||||||
233,
|
|
||||||
19.5058,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
55.0038,
|
|
||||||
233,
|
|
||||||
12.8843,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
67.8881,
|
|
||||||
233,
|
|
||||||
29.5304,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
294,
|
|
||||||
16.4126,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.9941,
|
|
||||||
294,
|
|
||||||
16.5039,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.498,
|
|
||||||
294,
|
|
||||||
19.5058,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
55.0038,
|
|
||||||
294,
|
|
||||||
12.8843,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
67.8881,
|
|
||||||
294,
|
|
||||||
29.5304,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
355,
|
|
||||||
16.4126,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.9941,
|
|
||||||
355,
|
|
||||||
16.5039,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.498,
|
|
||||||
355,
|
|
||||||
19.5058,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
55.0038,
|
|
||||||
355,
|
|
||||||
12.8843,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
67.8881,
|
|
||||||
355,
|
|
||||||
29.5304,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
416,
|
|
||||||
16.4126,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.9941,
|
|
||||||
416,
|
|
||||||
16.5039,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.498,
|
|
||||||
416,
|
|
||||||
19.5058,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
55.0038,
|
|
||||||
416,
|
|
||||||
12.8843,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
67.8881,
|
|
||||||
416,
|
|
||||||
29.5304,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "vehicles",
|
|
||||||
"viewportWidth": 996,
|
|
||||||
"width": 996,
|
|
||||||
"height": 451,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
7.4987,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.5753,
|
|
||||||
0,
|
|
||||||
12.4247,
|
|
||||||
32,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
48,
|
|
||||||
100,
|
|
||||||
403,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
67,
|
|
||||||
96.1847,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
119,
|
|
||||||
18.3531,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.2607,
|
|
||||||
119,
|
|
||||||
18.2762,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.537,
|
|
||||||
119,
|
|
||||||
21.1785,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
59.7154,
|
|
||||||
119,
|
|
||||||
14.7841,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
74.4996,
|
|
||||||
119,
|
|
||||||
23.5928,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
157,
|
|
||||||
18.3531,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.2607,
|
|
||||||
157,
|
|
||||||
18.2762,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.537,
|
|
||||||
157,
|
|
||||||
21.1785,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
59.7154,
|
|
||||||
157,
|
|
||||||
14.7841,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
74.4996,
|
|
||||||
157,
|
|
||||||
23.5928,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
212,
|
|
||||||
18.3531,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.2607,
|
|
||||||
212,
|
|
||||||
18.2762,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.537,
|
|
||||||
212,
|
|
||||||
21.1785,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
59.7154,
|
|
||||||
212,
|
|
||||||
14.7841,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
74.4996,
|
|
||||||
212,
|
|
||||||
23.5928,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
267,
|
|
||||||
18.3531,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.2607,
|
|
||||||
267,
|
|
||||||
18.2762,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.537,
|
|
||||||
267,
|
|
||||||
21.1785,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
59.7154,
|
|
||||||
267,
|
|
||||||
14.7841,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
74.4996,
|
|
||||||
267,
|
|
||||||
23.5928,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
322,
|
|
||||||
18.3531,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.2607,
|
|
||||||
322,
|
|
||||||
18.2762,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.537,
|
|
||||||
322,
|
|
||||||
21.1785,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
59.7154,
|
|
||||||
322,
|
|
||||||
14.7841,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
74.4996,
|
|
||||||
322,
|
|
||||||
23.5928,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
377,
|
|
||||||
18.3531,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.2607,
|
|
||||||
377,
|
|
||||||
18.2762,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.537,
|
|
||||||
377,
|
|
||||||
21.1785,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
59.7154,
|
|
||||||
377,
|
|
||||||
14.7841,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
74.4996,
|
|
||||||
377,
|
|
||||||
23.5928,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "567bad6080dc9ba9767c6e40a88559b9"
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,10 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 8px 14px;
|
padding: 8px 14px;
|
||||||
border: none;
|
/* 1px transparent border on ALL buttons so switching between primary
|
||||||
|
and secondary doesn't cause a 1px layout shift (the border is always
|
||||||
|
1px — only the colour changes). */
|
||||||
|
border: 1px solid transparent;
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 550;
|
font-weight: 550;
|
||||||
@@ -38,11 +41,13 @@
|
|||||||
|
|
||||||
.admin-btn-primary {
|
.admin-btn-primary {
|
||||||
background: var(--accent-color);
|
background: var(--accent-color);
|
||||||
|
border-color: var(--accent-color);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-btn-primary:hover:not(:disabled) {
|
.admin-btn-primary:hover:not(:disabled) {
|
||||||
background: var(--accent-hover);
|
background: var(--accent-hover);
|
||||||
|
border-color: var(--accent-hover);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 4px 12px rgba(214, 48, 49, 0.3);
|
box-shadow: 0 4px 12px rgba(214, 48, 49, 0.3);
|
||||||
}
|
}
|
||||||
@@ -75,6 +80,24 @@
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-btn-danger {
|
||||||
|
background: var(--danger);
|
||||||
|
border-color: var(--danger);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-btn-danger:hover:not(:disabled) {
|
||||||
|
background: color-mix(in srgb, var(--danger) 88%, #000);
|
||||||
|
border-color: color-mix(in srgb, var(--danger) 88%, #000);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px color-mix(in srgb, var(--danger) 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-btn-danger .admin-spinner {
|
||||||
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
|
border-top-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-btn-icon {
|
.admin-btn-icon {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -41,6 +41,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Danger card — highlighted border for alert/important cards */
|
||||||
|
.admin-card-danger {
|
||||||
|
border: 2px solid var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
Badges
|
Badges
|
||||||
============================================================================ */
|
============================================================================ */
|
||||||
@@ -123,6 +128,16 @@
|
|||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-badge-incoming {
|
||||||
|
background: var(--success-soft);
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-badge-outgoing {
|
||||||
|
background: var(--info-soft);
|
||||||
|
color: var(--info);
|
||||||
|
}
|
||||||
|
|
||||||
/* Status Badges - Leave Requests */
|
/* Status Badges - Leave Requests */
|
||||||
.badge-pending {
|
.badge-pending {
|
||||||
background: color-mix(in srgb, var(--warning) 15%, transparent);
|
background: color-mix(in srgb, var(--warning) 15%, transparent);
|
||||||
@@ -716,6 +731,7 @@
|
|||||||
.admin-kpi-grid {
|
.admin-kpi-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.875rem;
|
gap: 0.875rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-kpi-4 {
|
.admin-kpi-4 {
|
||||||
@@ -923,3 +939,163 @@
|
|||||||
max-height: 80px;
|
max-height: 80px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
Search Bar & Filters
|
||||||
|
============================================================================ */
|
||||||
|
|
||||||
|
.admin-search-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-search-bar .admin-form-input,
|
||||||
|
.admin-search-bar .admin-form-select {
|
||||||
|
flex: 1 1 140px;
|
||||||
|
min-width: 140px;
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-search-bar .react-datepicker-wrapper {
|
||||||
|
flex: 1 1 140px;
|
||||||
|
min-width: 140px;
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-search-bar .admin-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-search {
|
||||||
|
position: relative;
|
||||||
|
flex: 1 1 200px;
|
||||||
|
min-width: 180px;
|
||||||
|
max-width: 320px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-search svg {
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-search .admin-form-input {
|
||||||
|
padding-left: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.admin-search-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-search-bar .admin-form-input,
|
||||||
|
.admin-search-bar .admin-form-select {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-search-bar .react-datepicker-wrapper {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-search {
|
||||||
|
max-width: 100%;
|
||||||
|
flex: 1 1 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.admin-search-bar .admin-form-input,
|
||||||
|
.admin-search-bar .admin-form-select {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-search-bar .react-datepicker-wrapper {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
Item Picker (Warehouse)
|
||||||
|
============================================================================ */
|
||||||
|
|
||||||
|
.admin-item-picker-list {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
max-height: 240px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 var(--border-radius-sm) var(--border-radius-sm);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
padding: 4px 0;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-item-picker-list::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-item-picker-list::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-item-picker-list::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border-color);
|
||||||
|
border-radius: 99px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-item-picker-list::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-item-picker-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background var(--transition);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-item-picker-item:hover,
|
||||||
|
.admin-item-picker-item.active {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-item-picker-name {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-item-picker-number {
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-item-picker-qty {
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { forwardRef, useMemo } from "react";
|
import { forwardRef } from "react";
|
||||||
import DatePicker, { registerLocale } from "react-datepicker";
|
import DatePicker, { registerLocale } from "react-datepicker";
|
||||||
import { cs } from "date-fns/locale";
|
import { cs } from "date-fns/locale";
|
||||||
import { parse, format } from "date-fns";
|
import { parse, format } from "date-fns";
|
||||||
@@ -124,7 +124,7 @@ export default function AdminDatePicker({
|
|||||||
disabled,
|
disabled,
|
||||||
placeholder,
|
placeholder,
|
||||||
}: AdminDatePickerProps) {
|
}: AdminDatePickerProps) {
|
||||||
const useNative = useMemo(() => isTouchDevice(), []);
|
const useNative = isTouchDevice();
|
||||||
|
|
||||||
if (useNative) {
|
if (useNative) {
|
||||||
return (
|
return (
|
||||||
@@ -178,15 +178,12 @@ export default function AdminDatePicker({
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const customInput = useMemo(
|
const customInput = (
|
||||||
() => (
|
|
||||||
<CustomInput
|
<CustomInput
|
||||||
required={required}
|
required={required}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
),
|
|
||||||
[required, placeholder, disabled],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const commonProps = {
|
const commonProps = {
|
||||||
@@ -194,6 +191,7 @@ export default function AdminDatePicker({
|
|||||||
onChange: handleChange,
|
onChange: handleChange,
|
||||||
locale: "cs",
|
locale: "cs",
|
||||||
customInput,
|
customInput,
|
||||||
|
placeholderText: placeholder,
|
||||||
minDate: parseMinMax(minDate),
|
minDate: parseMinMax(minDate),
|
||||||
maxDate: parseMinMax(maxDate),
|
maxDate: parseMinMax(maxDate),
|
||||||
popperPlacement: "bottom-start" as const,
|
popperPlacement: "bottom-start" as const,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
formatDate,
|
formatDate,
|
||||||
formatDatetime,
|
formatDatetime,
|
||||||
@@ -8,34 +8,13 @@ import {
|
|||||||
getLeaveTypeName,
|
getLeaveTypeName,
|
||||||
getLeaveTypeBadgeClass,
|
getLeaveTypeBadgeClass,
|
||||||
} from "../utils/attendanceHelpers";
|
} from "../utils/attendanceHelpers";
|
||||||
|
import type { AttendanceRecord as HookAttendanceRecord } from "../hooks/useAttendanceAdmin";
|
||||||
|
|
||||||
interface ProjectLog {
|
interface AttendanceRecord extends HookAttendanceRecord {
|
||||||
id?: number;
|
|
||||||
project_id?: number;
|
|
||||||
project_name?: string;
|
|
||||||
started_at?: string;
|
|
||||||
ended_at?: string | null;
|
|
||||||
hours?: string | number | null;
|
|
||||||
minutes?: string | number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AttendanceRecord {
|
|
||||||
id: number;
|
|
||||||
shift_date: string;
|
|
||||||
user_name: string;
|
|
||||||
leave_type?: string;
|
|
||||||
leave_hours?: number;
|
|
||||||
arrival_time?: string | null;
|
|
||||||
departure_time?: string | null;
|
|
||||||
break_start?: string | null;
|
|
||||||
break_end?: string | null;
|
|
||||||
arrival_lat?: number | string | null;
|
arrival_lat?: number | string | null;
|
||||||
arrival_lng?: number | string | null;
|
arrival_lng?: number | string | null;
|
||||||
departure_lat?: number | string | null;
|
departure_lat?: number | string | null;
|
||||||
departure_lng?: number | string | null;
|
departure_lng?: number | string | null;
|
||||||
project_name?: string;
|
|
||||||
project_logs?: ProjectLog[];
|
|
||||||
notes?: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AttendanceShiftTableProps {
|
interface AttendanceShiftTableProps {
|
||||||
@@ -51,7 +30,7 @@ function formatBreak(record: AttendanceRecord): string {
|
|||||||
if (record.break_start) {
|
if (record.break_start) {
|
||||||
return `${formatTime(record.break_start)} - ?`;
|
return `${formatTime(record.break_start)} - ?`;
|
||||||
}
|
}
|
||||||
return "\u2014";
|
return "—";
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderProjectCell(record: AttendanceRecord): React.ReactNode {
|
function renderProjectCell(record: AttendanceRecord): React.ReactNode {
|
||||||
@@ -97,7 +76,7 @@ function renderProjectCell(record: AttendanceRecord): React.ReactNode {
|
|||||||
>
|
>
|
||||||
{log.project_name || `#${log.project_id}`}{" "}
|
{log.project_name || `#${log.project_id}`}{" "}
|
||||||
{durationValid
|
{durationValid
|
||||||
? `(${h}:${String(m).padStart(2, "0")}h${isActive ? " \u25B8" : ""})`
|
? `(${h}:${String(m).padStart(2, "0")}h${isActive ? " ▸" : ""})`
|
||||||
: "—"}
|
: "—"}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@@ -115,7 +94,7 @@ function renderProjectCell(record: AttendanceRecord): React.ReactNode {
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return "\u2014";
|
return "—";
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AttendanceShiftTable({
|
export default function AttendanceShiftTable({
|
||||||
@@ -156,7 +135,10 @@ export default function AttendanceShiftTable({
|
|||||||
const workMinutes = isLeave
|
const workMinutes = isLeave
|
||||||
? (record.leave_hours != null ? Number(record.leave_hours) : 8) *
|
? (record.leave_hours != null ? Number(record.leave_hours) : 8) *
|
||||||
60
|
60
|
||||||
: calculateWorkMinutes(record);
|
: calculateWorkMinutes({
|
||||||
|
...record,
|
||||||
|
notes: record.notes ?? undefined,
|
||||||
|
});
|
||||||
const hasLocation =
|
const hasLocation =
|
||||||
(record.arrival_lat && record.arrival_lng) ||
|
(record.arrival_lat && record.arrival_lng) ||
|
||||||
(record.departure_lat && record.departure_lng);
|
(record.departure_lat && record.departure_lng);
|
||||||
@@ -173,18 +155,16 @@ export default function AttendanceShiftTable({
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="admin-mono">
|
<td className="admin-mono">
|
||||||
{isLeave ? "\u2014" : formatDatetime(record.arrival_time)}
|
{isLeave ? "—" : formatDatetime(record.arrival_time)}
|
||||||
</td>
|
</td>
|
||||||
<td className="admin-mono">
|
<td className="admin-mono">
|
||||||
{isLeave ? "\u2014" : formatBreak(record)}
|
{isLeave ? "—" : formatBreak(record)}
|
||||||
</td>
|
</td>
|
||||||
<td className="admin-mono">
|
<td className="admin-mono">
|
||||||
{isLeave ? "\u2014" : formatDatetime(record.departure_time)}
|
{isLeave ? "—" : formatDatetime(record.departure_time)}
|
||||||
</td>
|
</td>
|
||||||
<td className="admin-mono">
|
<td className="admin-mono">
|
||||||
{workMinutes > 0
|
{workMinutes > 0 ? `${formatMinutes(workMinutes)} h` : "—"}
|
||||||
? `${formatMinutes(workMinutes)} h`
|
|
||||||
: "\u2014"}
|
|
||||||
</td>
|
</td>
|
||||||
<td>{renderProjectCell(record)}</td>
|
<td>{renderProjectCell(record)}</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -195,10 +175,10 @@ export default function AttendanceShiftTable({
|
|||||||
title="Zobrazit polohu"
|
title="Zobrazit polohu"
|
||||||
aria-label="Zobrazit polohu"
|
aria-label="Zobrazit polohu"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">{"\uD83D\uDCCD"}</span>
|
<span aria-hidden="true">{"📍"}</span>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
"\u2014"
|
"—"
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ReactNode } from "react";
|
import { useEffect, type ReactNode } from "react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
|
||||||
interface ConfirmModalProps {
|
interface ConfirmModalProps {
|
||||||
@@ -91,6 +91,19 @@ export default function ConfirmModal({
|
|||||||
confirmVariant,
|
confirmVariant,
|
||||||
loading,
|
loading,
|
||||||
}: ConfirmModalProps) {
|
}: ConfirmModalProps) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
|
||||||
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
|
if (e.key === "Escape" && !loading) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [isOpen, loading, onClose]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
|
|||||||
118
src/admin/components/FormModal.tsx
Normal file
118
src/admin/components/FormModal.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { useEffect, type ReactNode, type FormEvent } from "react";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import useModalLock from "../hooks/useModalLock";
|
||||||
|
|
||||||
|
export interface FormModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit?: () => void; // called when user clicks primary button (only if provided)
|
||||||
|
title: string;
|
||||||
|
children: ReactNode; // modal body (form fields)
|
||||||
|
submitLabel?: string; // primary button text
|
||||||
|
cancelLabel?: string; // cancel button text
|
||||||
|
loading?: boolean;
|
||||||
|
hideFooter?: boolean; // for "view-only" modals
|
||||||
|
size?: "sm" | "md" | "lg"; // optional
|
||||||
|
closeOnBackdrop?: boolean; // default true
|
||||||
|
closeOnEsc?: boolean; // default true
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FormModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
submitLabel = "Uložit",
|
||||||
|
cancelLabel = "Zrušit",
|
||||||
|
loading = false,
|
||||||
|
hideFooter = false,
|
||||||
|
size = "md",
|
||||||
|
closeOnBackdrop = true,
|
||||||
|
closeOnEsc = true,
|
||||||
|
}: FormModalProps) {
|
||||||
|
useModalLock(isOpen);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen || !closeOnEsc) return;
|
||||||
|
|
||||||
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
|
if (e.key === "Escape" && !loading) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [isOpen, closeOnEsc, loading, onClose]);
|
||||||
|
|
||||||
|
const handleBackdropClick = () => {
|
||||||
|
if (closeOnBackdrop && !loading) onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrimaryClick = (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (onSubmit) onSubmit();
|
||||||
|
};
|
||||||
|
|
||||||
|
const titleId = "form-modal-title";
|
||||||
|
const modalClassName =
|
||||||
|
size === "lg" ? "admin-modal admin-modal-lg" : "admin-modal";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<motion.div
|
||||||
|
className="admin-modal-overlay"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<div className="admin-modal-backdrop" onClick={handleBackdropClick} />
|
||||||
|
<motion.div
|
||||||
|
className={modalClassName}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby={titleId}
|
||||||
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<div className="admin-modal-header">
|
||||||
|
<h2 id={titleId} className="admin-modal-title">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="admin-modal-body">{children}</div>
|
||||||
|
|
||||||
|
{!hideFooter && (
|
||||||
|
<div className="admin-modal-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => !loading && onClose()}
|
||||||
|
className="admin-btn admin-btn-secondary"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{cancelLabel}
|
||||||
|
</button>
|
||||||
|
{onSubmit && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handlePrimaryClick}
|
||||||
|
className="admin-btn admin-btn-primary"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? "Zpracování..." : submitLabel}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
210
src/admin/components/PlanCategoriesModal.tsx
Normal file
210
src/admin/components/PlanCategoriesModal.tsx
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import FormModal from "./FormModal";
|
||||||
|
import { useApiMutation } from "../lib/queries/mutations";
|
||||||
|
import { useAlert } from "../context/AlertContext";
|
||||||
|
import { PlanCategory } from "../lib/queries/plan";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
categories: PlanCategory[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_NEW_COLOR = "#2563eb";
|
||||||
|
|
||||||
|
export default function PlanCategoriesModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
categories,
|
||||||
|
}: Props) {
|
||||||
|
const alert = useAlert();
|
||||||
|
const [newLabel, setNewLabel] = useState("");
|
||||||
|
const [newColor, setNewColor] = useState(DEFAULT_NEW_COLOR);
|
||||||
|
// Id of the row showing the inline "really delete?" confirm, or null.
|
||||||
|
const [confirmDeleteId, setConfirmDeleteId] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const createCat = useApiMutation<
|
||||||
|
{ label: string; color: string },
|
||||||
|
PlanCategory
|
||||||
|
>({
|
||||||
|
url: "/api/admin/plan/categories",
|
||||||
|
method: "POST",
|
||||||
|
invalidate: ["plan", "dashboard", "audit-log"],
|
||||||
|
});
|
||||||
|
const updateCat = useApiMutation<
|
||||||
|
{ id: number; label?: string; color?: string; is_active?: boolean },
|
||||||
|
PlanCategory
|
||||||
|
>({
|
||||||
|
url: (v) => `/api/admin/plan/categories/${v.id}`,
|
||||||
|
method: "PATCH",
|
||||||
|
invalidate: ["plan", "dashboard", "audit-log"],
|
||||||
|
});
|
||||||
|
const deleteCat = useApiMutation<{ id: number }, { ok: true }>({
|
||||||
|
url: (v) => `/api/admin/plan/categories/${v.id}`,
|
||||||
|
method: "DELETE",
|
||||||
|
invalidate: ["plan", "dashboard", "audit-log"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
const label = newLabel.trim();
|
||||||
|
if (!label) {
|
||||||
|
alert.error("Zadejte název kategorie");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
createCat.mutate(
|
||||||
|
{ label, color: newColor },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
setNewLabel("");
|
||||||
|
setNewColor(DEFAULT_NEW_COLOR);
|
||||||
|
},
|
||||||
|
onError: (e) => alert.error(e.message),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const patch = (
|
||||||
|
id: number,
|
||||||
|
body: { label?: string; color?: string; is_active?: boolean },
|
||||||
|
) => {
|
||||||
|
updateCat.mutate(
|
||||||
|
{ id, ...body },
|
||||||
|
{ onError: (e) => alert.error(e.message) },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (id: number) => {
|
||||||
|
deleteCat.mutate(
|
||||||
|
{ id },
|
||||||
|
{
|
||||||
|
onSuccess: () => setConfirmDeleteId(null),
|
||||||
|
// Server blocks deleting a category that is still in use (409) — show
|
||||||
|
// the Czech message ("…můžete ji skrýt") and keep the row.
|
||||||
|
onError: (e) => {
|
||||||
|
alert.error(e.message);
|
||||||
|
setConfirmDeleteId(null);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const busy =
|
||||||
|
createCat.isPending || updateCat.isPending || deleteCat.isPending;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormModal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
title="Správa kategorií"
|
||||||
|
hideFooter
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
<div className="plan-cat-manager">
|
||||||
|
{categories.map((c) => (
|
||||||
|
<div
|
||||||
|
key={c.id}
|
||||||
|
className={`plan-cat-row${c.is_active ? "" : " plan-cat-row--inactive"}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
className="plan-cat-color"
|
||||||
|
// Uncontrolled + patch on blur (like the rename input): the
|
||||||
|
// native color picker fires onChange on every drag tick, which
|
||||||
|
// would spam PATCH requests and the audit log. onBlur commits
|
||||||
|
// once when the picker closes.
|
||||||
|
defaultValue={c.color}
|
||||||
|
key={c.color}
|
||||||
|
disabled={busy}
|
||||||
|
onBlur={(e) => {
|
||||||
|
if (e.target.value !== c.color) {
|
||||||
|
patch(c.id, { color: e.target.value });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
aria-label={`Barva – ${c.label}`}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="admin-form-input"
|
||||||
|
defaultValue={c.label}
|
||||||
|
disabled={busy}
|
||||||
|
onBlur={(e) => {
|
||||||
|
const v = e.target.value.trim();
|
||||||
|
if (v && v !== c.label) patch(c.id, { label: v });
|
||||||
|
}}
|
||||||
|
aria-label={`Název – ${c.label}`}
|
||||||
|
/>
|
||||||
|
{confirmDeleteId === c.id ? (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="admin-btn admin-btn-danger admin-btn-sm"
|
||||||
|
disabled={busy}
|
||||||
|
onClick={() => handleDelete(c.id)}
|
||||||
|
>
|
||||||
|
Opravdu smazat
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="admin-btn admin-btn-secondary admin-btn-sm"
|
||||||
|
disabled={busy}
|
||||||
|
onClick={() => setConfirmDeleteId(null)}
|
||||||
|
>
|
||||||
|
Zrušit
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="admin-btn admin-btn-secondary admin-btn-sm"
|
||||||
|
disabled={busy}
|
||||||
|
onClick={() => patch(c.id, { is_active: !c.is_active })}
|
||||||
|
>
|
||||||
|
{c.is_active ? "Skrýt" : "Obnovit"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="admin-btn admin-btn-danger admin-btn-sm plan-cat-delete"
|
||||||
|
disabled={busy}
|
||||||
|
onClick={() => setConfirmDeleteId(c.id)}
|
||||||
|
aria-label={`Smazat – ${c.label}`}
|
||||||
|
title="Smazat kategorii"
|
||||||
|
>
|
||||||
|
Smazat
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="plan-cat-row plan-cat-row--new">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
className="plan-cat-color"
|
||||||
|
value={newColor}
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(e) => setNewColor(e.target.value)}
|
||||||
|
aria-label="Barva nové kategorie"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="admin-form-input"
|
||||||
|
placeholder="Nová kategorie…"
|
||||||
|
value={newLabel}
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(e) => setNewLabel(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === "Enter" && handleAdd()}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="admin-btn admin-btn-primary admin-btn-sm"
|
||||||
|
disabled={busy}
|
||||||
|
onClick={handleAdd}
|
||||||
|
>
|
||||||
|
Přidat
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
482
src/admin/components/PlanCellModal.tsx
Normal file
482
src/admin/components/PlanCellModal.tsx
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import FormModal from "./FormModal";
|
||||||
|
import FormField from "./FormField";
|
||||||
|
import AdminDatePicker from "./AdminDatePicker";
|
||||||
|
import ConfirmModal from "./ConfirmModal";
|
||||||
|
import useReducedMotion from "../hooks/useReducedMotion";
|
||||||
|
import {
|
||||||
|
ResolvedCell,
|
||||||
|
PlanCategory,
|
||||||
|
planCategoryLabel,
|
||||||
|
cellProjectLabel,
|
||||||
|
} from "../lib/queries/plan";
|
||||||
|
import { formatDate } from "../utils/formatters";
|
||||||
|
|
||||||
|
interface Project {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
project_number?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PlanCellModalMode =
|
||||||
|
| { kind: "closed" }
|
||||||
|
| { kind: "create"; userId: number; date: string }
|
||||||
|
| {
|
||||||
|
kind: "edit-range";
|
||||||
|
entryId: number;
|
||||||
|
userId: number;
|
||||||
|
date: string;
|
||||||
|
range: {
|
||||||
|
date_from: string;
|
||||||
|
date_to: string;
|
||||||
|
project_id: number | null;
|
||||||
|
category: string;
|
||||||
|
note: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
kind: "edit-override";
|
||||||
|
overrideId: number;
|
||||||
|
userId: number;
|
||||||
|
date: string;
|
||||||
|
cell: ResolvedCell;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
kind: "day-in-range";
|
||||||
|
entryId: number;
|
||||||
|
userId: number;
|
||||||
|
date: string;
|
||||||
|
range: {
|
||||||
|
date_from: string;
|
||||||
|
date_to: string;
|
||||||
|
project_id: number | null;
|
||||||
|
category: string;
|
||||||
|
note: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| { kind: "view"; userId: number; date: string; cell: ResolvedCell };
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
mode: PlanCellModalMode;
|
||||||
|
projects: Project[];
|
||||||
|
categories: PlanCategory[];
|
||||||
|
onClose: () => void;
|
||||||
|
onSaveEntry: (body: any) => Promise<void>;
|
||||||
|
onUpdateEntry: (id: number, body: any) => Promise<void>;
|
||||||
|
onDeleteEntry: (id: number) => Promise<void>;
|
||||||
|
onSaveOverride: (body: any) => Promise<void>;
|
||||||
|
onUpdateOverride: (id: number, body: any) => Promise<void>;
|
||||||
|
onDeleteOverride: (id: number) => Promise<void>;
|
||||||
|
onCreateOverrideFromRange: (
|
||||||
|
entryId: number,
|
||||||
|
userId: number,
|
||||||
|
date: string,
|
||||||
|
) => Promise<void>;
|
||||||
|
onSwitchToEditRange: (entryId: number, userId: number, date: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PlanCellModal(props: Props) {
|
||||||
|
const { open, mode, onClose } = props;
|
||||||
|
if (!open || mode.kind === "closed") return null;
|
||||||
|
if (mode.kind === "view") return <ViewModal {...props} />;
|
||||||
|
if (mode.kind === "day-in-range")
|
||||||
|
return <DayInRangeModal {...props} mode={mode} />;
|
||||||
|
return <EditForm {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditForm(props: Props) {
|
||||||
|
const {
|
||||||
|
mode,
|
||||||
|
onClose,
|
||||||
|
onSaveEntry,
|
||||||
|
onUpdateEntry,
|
||||||
|
onDeleteEntry,
|
||||||
|
onSaveOverride,
|
||||||
|
onUpdateOverride,
|
||||||
|
onDeleteOverride,
|
||||||
|
projects,
|
||||||
|
} = props;
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||||
|
|
||||||
|
// Narrow mode to the kinds EditForm actually handles. The call site in
|
||||||
|
// PlanCellModal only passes "create" | "edit-range" | "edit-override" but
|
||||||
|
// the parameter type is the full union, so we narrow explicitly here.
|
||||||
|
if (
|
||||||
|
mode.kind !== "create" &&
|
||||||
|
mode.kind !== "edit-range" &&
|
||||||
|
mode.kind !== "edit-override"
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOverride = mode.kind === "edit-override";
|
||||||
|
const activeCategories = props.categories.filter((c) => c.is_active);
|
||||||
|
|
||||||
|
const initial = (() => {
|
||||||
|
if (mode.kind === "create") {
|
||||||
|
return {
|
||||||
|
date_from: mode.date,
|
||||||
|
date_to: mode.date,
|
||||||
|
is_range: false,
|
||||||
|
project_id: null as number | null,
|
||||||
|
// Default to "work" when it's active, otherwise the first active
|
||||||
|
// category, so a new entry never starts on a retired key.
|
||||||
|
category: activeCategories.some((c) => c.key === "work")
|
||||||
|
? "work"
|
||||||
|
: (activeCategories[0]?.key ?? "work"),
|
||||||
|
note: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (mode.kind === "edit-range") {
|
||||||
|
return {
|
||||||
|
date_from: mode.range.date_from,
|
||||||
|
date_to: mode.range.date_to,
|
||||||
|
is_range: mode.range.date_from !== mode.range.date_to,
|
||||||
|
project_id: mode.range.project_id,
|
||||||
|
category: mode.range.category,
|
||||||
|
note: mode.range.note,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// mode.kind === "edit-override" (narrowed above)
|
||||||
|
return {
|
||||||
|
date_from: mode.date,
|
||||||
|
date_to: mode.date,
|
||||||
|
is_range: false,
|
||||||
|
project_id: mode.cell.project_id,
|
||||||
|
category: mode.cell.category,
|
||||||
|
note: mode.cell.note,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
const [dateFrom, setDateFrom] = useState(initial.date_from);
|
||||||
|
const [dateTo, setDateTo] = useState(initial.date_to);
|
||||||
|
const [isRange, setIsRange] = useState(initial.is_range);
|
||||||
|
const [projectId, setProjectId] = useState<number | null>(initial.project_id);
|
||||||
|
const [category, setCategory] = useState(initial.category);
|
||||||
|
const [note, setNote] = useState(initial.note);
|
||||||
|
|
||||||
|
const title =
|
||||||
|
mode.kind === "create"
|
||||||
|
? "Nový záznam plánu"
|
||||||
|
: mode.kind === "edit-range"
|
||||||
|
? "Upravit rozsah"
|
||||||
|
: "Upravit přepsání dne";
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
|
if (mode.kind === "create") {
|
||||||
|
const userId = mode.userId;
|
||||||
|
await onSaveEntry({
|
||||||
|
user_id: userId,
|
||||||
|
date_from: dateFrom,
|
||||||
|
date_to: isRange ? dateTo : dateFrom,
|
||||||
|
project_id: projectId,
|
||||||
|
category,
|
||||||
|
note,
|
||||||
|
});
|
||||||
|
} else if (mode.kind === "edit-range") {
|
||||||
|
await onUpdateEntry(mode.entryId, {
|
||||||
|
date_from: dateFrom,
|
||||||
|
date_to: isRange ? dateTo : dateFrom,
|
||||||
|
project_id: projectId,
|
||||||
|
category,
|
||||||
|
note,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await onUpdateOverride(mode.overrideId, {
|
||||||
|
project_id: projectId,
|
||||||
|
category,
|
||||||
|
note,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
|
if (mode.kind === "edit-range") {
|
||||||
|
await onDeleteEntry(mode.entryId);
|
||||||
|
} else if (mode.kind === "edit-override") {
|
||||||
|
await onDeleteOverride(mode.overrideId);
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormModal
|
||||||
|
isOpen
|
||||||
|
title={title}
|
||||||
|
onClose={onClose}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
submitLabel={mode.kind === "create" ? "Vytvořit" : "Uložit"}
|
||||||
|
loading={submitting}
|
||||||
|
footerLeft={
|
||||||
|
mode.kind !== "create" ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="admin-btn admin-btn-secondary"
|
||||||
|
onClick={() => setConfirmDelete(true)}
|
||||||
|
disabled={submitting}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden
|
||||||
|
>
|
||||||
|
<polyline points="3 6 5 6 21 6" />
|
||||||
|
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
||||||
|
<path d="M10 11v6" />
|
||||||
|
<path d="M14 11v6" />
|
||||||
|
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
|
||||||
|
</svg>
|
||||||
|
Smazat
|
||||||
|
</button>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FormField label="Datum od">
|
||||||
|
<AdminDatePicker
|
||||||
|
value={dateFrom}
|
||||||
|
onChange={setDateFrom}
|
||||||
|
disabled={isOverride}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
{!isOverride && (
|
||||||
|
<FormField label="Rozsah dnů">
|
||||||
|
<label className="admin-form-checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isRange}
|
||||||
|
onChange={(e) => setIsRange(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span>Více dní</span>
|
||||||
|
</label>
|
||||||
|
</FormField>
|
||||||
|
)}
|
||||||
|
{!isOverride && isRange && (
|
||||||
|
<FormField label="Datum do">
|
||||||
|
<AdminDatePicker value={dateTo} onChange={setDateTo} />
|
||||||
|
</FormField>
|
||||||
|
)}
|
||||||
|
<FormField label="Projekt">
|
||||||
|
<select
|
||||||
|
className="admin-form-select"
|
||||||
|
value={projectId ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setProjectId(e.target.value ? Number(e.target.value) : null)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="">— bez projektu —</option>
|
||||||
|
{projects.map((p) => (
|
||||||
|
<option key={p.id} value={p.id}>
|
||||||
|
{p.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Kategorie">
|
||||||
|
<select
|
||||||
|
className="admin-form-select"
|
||||||
|
value={category}
|
||||||
|
onChange={(e) => setCategory(e.target.value)}
|
||||||
|
>
|
||||||
|
{/* If the row's current category was retired/hidden, still show it
|
||||||
|
(marked) so the select reflects reality instead of silently
|
||||||
|
snapping to the first active option. The server only re-checks
|
||||||
|
the category if it changes, so keeping it saves fine. */}
|
||||||
|
{category && !activeCategories.some((c) => c.key === category) && (
|
||||||
|
<option value={category}>
|
||||||
|
{planCategoryLabel(
|
||||||
|
category,
|
||||||
|
Object.fromEntries(props.categories.map((x) => [x.key, x])),
|
||||||
|
)}{" "}
|
||||||
|
(skrytá)
|
||||||
|
</option>
|
||||||
|
)}
|
||||||
|
{activeCategories.map((c) => (
|
||||||
|
<option key={c.key} value={c.key}>
|
||||||
|
{c.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Text poznámky (volitelný)">
|
||||||
|
<textarea
|
||||||
|
className="admin-form-textarea"
|
||||||
|
value={note}
|
||||||
|
onChange={(e) => setNote(e.target.value)}
|
||||||
|
maxLength={500}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</FormModal>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={confirmDelete}
|
||||||
|
title="Smazat záznam plánu?"
|
||||||
|
message="Tato akce je nevratná (záznam bude soft-delete)."
|
||||||
|
confirmText="Smazat"
|
||||||
|
type="danger"
|
||||||
|
confirmVariant="danger"
|
||||||
|
loading={submitting}
|
||||||
|
onClose={() => setConfirmDelete(false)}
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DayInRangeModal(
|
||||||
|
props: Props & { mode: Extract<PlanCellModalMode, { kind: "day-in-range" }> },
|
||||||
|
) {
|
||||||
|
const { mode, onClose, onCreateOverrideFromRange, onSwitchToEditRange } =
|
||||||
|
props;
|
||||||
|
const reducedMotion = useReducedMotion();
|
||||||
|
|
||||||
|
// Two clear paths: edit the whole range (default, safe) or carve out
|
||||||
|
// a one-day override (deviates from the plan). Close = no action.
|
||||||
|
const choices = [
|
||||||
|
{
|
||||||
|
key: "edit-range",
|
||||||
|
title: "Upravit celý rozsah",
|
||||||
|
desc: `Změní projekt, kategorii nebo poznámku pro všechny dny v rozsahu ${mode.range.date_from} – ${mode.range.date_to}.`,
|
||||||
|
tone: "primary" as const,
|
||||||
|
onClick: () => onSwitchToEditRange(mode.entryId, mode.userId, mode.date),
|
||||||
|
cta: "Upravit rozsah",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "override-day",
|
||||||
|
title: "Upravit pouze tento den",
|
||||||
|
desc: `Vytvoří přepsání pro ${mode.range.date_from === mode.date ? "tento" : mode.date}; zbytek rozsahu zůstane beze změny.`,
|
||||||
|
tone: "secondary" as const,
|
||||||
|
onClick: async () => {
|
||||||
|
await onCreateOverrideFromRange(mode.entryId, mode.userId, mode.date);
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
cta: "Vytvořit přepsání",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormModal
|
||||||
|
isOpen
|
||||||
|
title="Den je součástí rozsahu"
|
||||||
|
onClose={onClose}
|
||||||
|
hideFooter
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
<p className="plan-modal-intro">
|
||||||
|
Den <strong>{mode.date}</strong> je součástí rozsahu{" "}
|
||||||
|
<strong>
|
||||||
|
{mode.range.date_from} – {mode.range.date_to}
|
||||||
|
</strong>
|
||||||
|
. Vyberte, co chcete udělat:
|
||||||
|
</p>
|
||||||
|
<ul className="plan-choices">
|
||||||
|
<AnimatePresence>
|
||||||
|
{choices.map((c, i) => (
|
||||||
|
<motion.li
|
||||||
|
key={c.key}
|
||||||
|
className={`plan-choice plan-choice-${c.tone}`}
|
||||||
|
initial={reducedMotion ? false : { opacity: 0, y: 8 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={reducedMotion ? undefined : { opacity: 0, y: -4 }}
|
||||||
|
transition={{
|
||||||
|
duration: reducedMotion ? 0 : 0.22,
|
||||||
|
delay: reducedMotion ? 0 : 0.04 + i * 0.05,
|
||||||
|
ease: [0.16, 1, 0.3, 1],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="plan-choice-body">
|
||||||
|
<div className="plan-choice-title">{c.title}</div>
|
||||||
|
<div className="plan-choice-desc">{c.desc}</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={
|
||||||
|
c.tone === "primary"
|
||||||
|
? "admin-btn admin-btn-primary"
|
||||||
|
: "admin-btn admin-btn-secondary"
|
||||||
|
}
|
||||||
|
onClick={c.onClick}
|
||||||
|
>
|
||||||
|
{c.cta}
|
||||||
|
</button>
|
||||||
|
</motion.li>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
</ul>
|
||||||
|
<div className="plan-modal-actions plan-modal-actions--solo">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="admin-btn admin-btn-secondary"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
Zavřít — ponechat rozsah beze změny
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</FormModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ViewModal(props: Props) {
|
||||||
|
const { mode, onClose, projects, categories } = props;
|
||||||
|
if (mode.kind !== "view") return null;
|
||||||
|
const c = mode.cell;
|
||||||
|
// Prefer the server-embedded project label; fall back to the projects-list
|
||||||
|
// lookup only for cells that predate the embed (stale cache).
|
||||||
|
const fallback = c.project_id
|
||||||
|
? (projects.find((p) => p.id === c.project_id) ?? null)
|
||||||
|
: null;
|
||||||
|
const projectLabel =
|
||||||
|
cellProjectLabel(c) ??
|
||||||
|
(fallback
|
||||||
|
? fallback.project_number
|
||||||
|
? `${fallback.project_number} — ${fallback.name}`
|
||||||
|
: fallback.name
|
||||||
|
: null);
|
||||||
|
return (
|
||||||
|
<FormModal isOpen title="Detail plánu" onClose={onClose} hideFooter>
|
||||||
|
<p>
|
||||||
|
<strong>Datum:</strong> {formatDate(c.shift_date)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Kategorie:</strong>{" "}
|
||||||
|
{planCategoryLabel(
|
||||||
|
c.category,
|
||||||
|
Object.fromEntries(categories.map((x) => [x.key, x])),
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Projekt:</strong> {projectLabel || "—"}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Text:</strong> {c.note || "—"}
|
||||||
|
</p>
|
||||||
|
{c.rangeFrom && c.rangeTo && (
|
||||||
|
<p>
|
||||||
|
<strong>Patří do rozsahu:</strong> {formatDate(c.rangeFrom)} –{" "}
|
||||||
|
{formatDate(c.rangeTo)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
260
src/admin/components/PlanGrid.tsx
Normal file
260
src/admin/components/PlanGrid.tsx
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import type { CSSProperties } from "react";
|
||||||
|
import {
|
||||||
|
GridData,
|
||||||
|
ResolvedCell,
|
||||||
|
planCategoryLabel,
|
||||||
|
categoryMap,
|
||||||
|
PlanCategory,
|
||||||
|
} from "../lib/queries/plan";
|
||||||
|
import type { Project } from "../lib/queries/projects";
|
||||||
|
import PlanRangeChips from "./PlanRangeChips";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: GridData | undefined;
|
||||||
|
canEdit: boolean;
|
||||||
|
projects: Project[];
|
||||||
|
categories: PlanCategory[];
|
||||||
|
// The (userId, date) of the most recent successful mutation. The
|
||||||
|
// matching cell gets a one-shot highlight pulse. `nonce` is part of
|
||||||
|
// the value so back-to-back mutations on the same cell re-trigger
|
||||||
|
// the animation (React needs a new key to re-mount the class).
|
||||||
|
pulseKey?: { userId: number; date: string; nonce: number } | null;
|
||||||
|
onCellClick: (
|
||||||
|
userId: number,
|
||||||
|
date: string,
|
||||||
|
cell: ResolvedCell | null,
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full Czech weekday names, indexed by Date.getUTCDay() (0 = Sunday).
|
||||||
|
// Used in the date column "stamp" so the day name reads in full rather
|
||||||
|
// than as a 2-letter abbreviation — easier to scan when the planner
|
||||||
|
// spans several weeks.
|
||||||
|
const CZECH_WEEKDAYS = [
|
||||||
|
"Neděle",
|
||||||
|
"Pondělí",
|
||||||
|
"Úterý",
|
||||||
|
"Středa",
|
||||||
|
"Čtvrtek",
|
||||||
|
"Pátek",
|
||||||
|
"Sobota",
|
||||||
|
];
|
||||||
|
|
||||||
|
function eachDay(from: string, to: string): string[] {
|
||||||
|
const out: string[] = [];
|
||||||
|
const a = new Date(from + "T00:00:00.000Z");
|
||||||
|
const b = new Date(to + "T00:00:00.000Z");
|
||||||
|
for (let d = new Date(a); d <= b; d.setUTCDate(d.getUTCDate() + 1)) {
|
||||||
|
out.push(d.toISOString().slice(0, 10));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function czechWeekday(dateStr: string): string {
|
||||||
|
return CZECH_WEEKDAYS[new Date(dateStr + "T00:00:00.000Z").getUTCDay()];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWeekend(dateStr: string): boolean {
|
||||||
|
const day = new Date(dateStr + "T00:00:00.000Z").getUTCDay();
|
||||||
|
return day === 0 || day === 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Today's local date as YYYY-MM-DD. Used to mark the current row and to
|
||||||
|
// gate past-day editing. Uses local time (matches the project's date
|
||||||
|
// conventions in CLAUDE.md and the server's `assertNotPastDate` guard in
|
||||||
|
// src/services/plan.service.ts).
|
||||||
|
function todayIso(): string {
|
||||||
|
const d = new Date();
|
||||||
|
const y = d.getFullYear();
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(d.getDate()).padStart(2, "0");
|
||||||
|
return `${y}-${m}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// True when the given YYYY-MM-DD cell date is strictly before today (local).
|
||||||
|
// Used to gate the create / edit UI on past days — the server still enforces
|
||||||
|
// the rule with a 403, but the client should never put the user in the
|
||||||
|
// "I clicked, modal opened, error toast" state in the first place.
|
||||||
|
function isPastDate(dateStr: string, today: string): boolean {
|
||||||
|
return dateStr < today;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split a full name into first / last for the column header.
|
||||||
|
function splitName(full: string): { first: string; last: string } {
|
||||||
|
const parts = full.trim().split(/\s+/);
|
||||||
|
if (parts.length === 1) return { first: parts[0], last: "" };
|
||||||
|
return { first: parts[0], last: parts.slice(1).join(" ") };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Short role label for the column header sub-line.
|
||||||
|
function shortRole(role: string | null): string {
|
||||||
|
if (!role) return "";
|
||||||
|
// Keep it brief — column header is tight
|
||||||
|
if (/^admin$/i.test(role)) return "Admin";
|
||||||
|
if (/^viewer$/i.test(role)) return "Viewer";
|
||||||
|
return role.length > 10 ? role.slice(0, 9) + "…" : role;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PlanGrid({
|
||||||
|
data,
|
||||||
|
canEdit,
|
||||||
|
projects,
|
||||||
|
categories,
|
||||||
|
pulseKey,
|
||||||
|
onCellClick,
|
||||||
|
}: Props) {
|
||||||
|
const today = useMemo(() => todayIso(), []);
|
||||||
|
const catMap = useMemo(() => categoryMap(categories), [categories]);
|
||||||
|
|
||||||
|
if (!data)
|
||||||
|
return (
|
||||||
|
<div className="admin-loading">
|
||||||
|
<div className="admin-spinner" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
const days = eachDay(data.date_from, data.date_to);
|
||||||
|
const users = data.users;
|
||||||
|
// pulseKey?.nonce is included in the data-pulse attribute so a second
|
||||||
|
// mutation on the same cell re-triggers the animation (CSS animations
|
||||||
|
// don't restart unless the keyframe applies to a fresh element/class
|
||||||
|
// binding).
|
||||||
|
const pulseAttr = pulseKey
|
||||||
|
? `${pulseKey.userId}|${pulseKey.date}|${pulseKey.nonce}`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="plan-grid-wrap" data-pulse={pulseAttr}>
|
||||||
|
<table className="plan-grid">
|
||||||
|
{/* The colgroup is what actually controls column width in a
|
||||||
|
table — `min-width` on `<th>`/`<td>` is just a floor that
|
||||||
|
`table-layout: auto` happily blows past. The first column
|
||||||
|
gets a fixed width via the col element so the date stamp
|
||||||
|
doesn't get stretched to share space with person columns. */}
|
||||||
|
<colgroup>
|
||||||
|
<col className="plan-grid-date-col" />
|
||||||
|
{users.map((u) => (
|
||||||
|
<col key={u.id} />
|
||||||
|
))}
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="plan-grid-date-col">Datum</th>
|
||||||
|
{users.map((u) => {
|
||||||
|
const { first, last } = splitName(u.full_name);
|
||||||
|
return (
|
||||||
|
<th key={u.id}>
|
||||||
|
<span className="plan-person-head">
|
||||||
|
<span className="plan-person-dot" aria-hidden />
|
||||||
|
<span className="plan-person-name">
|
||||||
|
<strong title={u.full_name}>
|
||||||
|
{first}
|
||||||
|
{last ? ` ${last}` : ""}
|
||||||
|
</strong>
|
||||||
|
{shortRole(u.role_name) && (
|
||||||
|
<small>{shortRole(u.role_name)}</small>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{days.map((date) => {
|
||||||
|
const dow = czechWeekday(date);
|
||||||
|
const dayNum = date.slice(8, 10);
|
||||||
|
const isToday = date === today;
|
||||||
|
const trCls = [
|
||||||
|
isWeekend(date) ? "plan-grid-weekend" : "",
|
||||||
|
isToday ? "is-today" : "",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
return (
|
||||||
|
<tr key={date} className={trCls}>
|
||||||
|
<td className="plan-grid-date-col">
|
||||||
|
<span className="plan-date-stamp">
|
||||||
|
<span className="plan-date-daynum">{dayNum}</span>
|
||||||
|
<span className="plan-date-dow">{dow}</span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
{users.map((u) => {
|
||||||
|
const cell = data.cells[u.id]?.[date] ?? null;
|
||||||
|
const past = isPastDate(date, today);
|
||||||
|
// Past-day cells are read-only in the UI: an empty past
|
||||||
|
// cell is non-interactive (no create modal, no "+" hint),
|
||||||
|
// a past cell with data opens in view mode only. The
|
||||||
|
// server still enforces the past-date rule with a 403
|
||||||
|
// (defense in depth), but the click path here never
|
||||||
|
// reaches a create/edit submission for a past date.
|
||||||
|
const isLocked = !canEdit || past;
|
||||||
|
// `isPulsing` is true for the single (user, date) cell
|
||||||
|
// that the most recent successful mutation touched.
|
||||||
|
// CSS restarts the keyframe animation whenever the
|
||||||
|
// `nonce` changes (we embed it in data-pulse on the
|
||||||
|
// wrapper, see above), so back-to-back mutations on the
|
||||||
|
// same cell re-trigger the pulse.
|
||||||
|
const isPulsing =
|
||||||
|
!!pulseKey &&
|
||||||
|
pulseKey.userId === u.id &&
|
||||||
|
pulseKey.date === date;
|
||||||
|
let cls: string;
|
||||||
|
if (cell) {
|
||||||
|
cls = isLocked
|
||||||
|
? `plan-cell plan-cell--readonly${past ? " plan-cell--past" : ""}`
|
||||||
|
: "plan-cell";
|
||||||
|
} else {
|
||||||
|
cls = isLocked
|
||||||
|
? `plan-cell plan-cell--empty plan-cell--readonly${past ? " plan-cell--past" : ""}`
|
||||||
|
: "plan-cell plan-cell--empty";
|
||||||
|
}
|
||||||
|
if (isPulsing) cls += " plan-cell--pulse";
|
||||||
|
return (
|
||||||
|
<td key={u.id}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cls}
|
||||||
|
style={
|
||||||
|
cell
|
||||||
|
? ({
|
||||||
|
"--cat-color": catMap[cell.category]?.color,
|
||||||
|
} as CSSProperties)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onClick={() => onCellClick(u.id, date, cell)}
|
||||||
|
aria-label={
|
||||||
|
cell
|
||||||
|
? `${u.full_name}, ${date}, ${planCategoryLabel(cell.category, catMap)}`
|
||||||
|
: isLocked
|
||||||
|
? `${u.full_name}, ${date}, ${past ? "uplynulý den" : "prázdné"} — bez záznamu`
|
||||||
|
: `${u.full_name}, ${date}, prázdné — přidat záznam`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<PlanRangeChips
|
||||||
|
cell={cell}
|
||||||
|
project={
|
||||||
|
cell?.project_id
|
||||||
|
? (projects.find(
|
||||||
|
(p) => p.id === cell.project_id,
|
||||||
|
) ?? null)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
readonly={!canEdit}
|
||||||
|
categoryLabel={
|
||||||
|
cell ? planCategoryLabel(cell.category, catMap) : ""
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
42
src/admin/components/PlanRangeChips.tsx
Normal file
42
src/admin/components/PlanRangeChips.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { ResolvedCell, cellProjectLabel } from "../lib/queries/plan";
|
||||||
|
import type { Project } from "../lib/queries/projects";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
cell: ResolvedCell | null;
|
||||||
|
project: Project | null;
|
||||||
|
readonly?: boolean;
|
||||||
|
categoryLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PlanRangeChips({
|
||||||
|
cell,
|
||||||
|
project,
|
||||||
|
readonly,
|
||||||
|
categoryLabel,
|
||||||
|
}: Props) {
|
||||||
|
void readonly;
|
||||||
|
if (!cell) return null;
|
||||||
|
// Prefer the server-embedded project label (always present). Fall back to
|
||||||
|
// the looked-up `project` prop only if the cell predates the embed (stale
|
||||||
|
// cache).
|
||||||
|
const fallbackLabel = project
|
||||||
|
? project.project_number
|
||||||
|
? `${project.project_number} — ${project.name}`
|
||||||
|
: project.name
|
||||||
|
: null;
|
||||||
|
const projectLabel = cellProjectLabel(cell) ?? fallbackLabel;
|
||||||
|
const projectTitle = projectLabel ?? undefined;
|
||||||
|
return (
|
||||||
|
<div className="plan-chip-block">
|
||||||
|
<div className="plan-chip-line">
|
||||||
|
<span className="plan-chip">{categoryLabel}</span>
|
||||||
|
{projectLabel && (
|
||||||
|
<span className="plan-chip-project" title={projectTitle}>
|
||||||
|
{projectLabel}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{cell.note && <div className="plan-cell-note">{cell.note}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,8 +4,6 @@ import { projectFilesOptions } from "../lib/queries/projects";
|
|||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import ConfirmModal from "./ConfirmModal";
|
import ConfirmModal from "./ConfirmModal";
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
import { Skeleton } from "boneyard-js/react";
|
|
||||||
import ProjectFileManagerFixture from "../fixtures/ProjectFileManagerFixture";
|
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
@@ -472,13 +470,9 @@ export default function ProjectFileManager({
|
|||||||
|
|
||||||
if (filesLoading && items.length === 0 && !errorMessage) {
|
if (filesLoading && items.length === 0 && !errorMessage) {
|
||||||
return (
|
return (
|
||||||
<Skeleton
|
<div className="admin-loading">
|
||||||
name="project-file-manager"
|
<div className="admin-spinner" />
|
||||||
loading={filesLoading && items.length === 0}
|
</div>
|
||||||
fixture={<ProjectFileManagerFixture />}
|
|
||||||
>
|
|
||||||
<div />
|
|
||||||
</Skeleton>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,15 @@ export interface ShiftFormModalProps {
|
|||||||
|
|
||||||
function ProjectTimeStatus({ form, projectLogs }: ProjectTimeStatusProps) {
|
function ProjectTimeStatus({ form, projectLogs }: ProjectTimeStatusProps) {
|
||||||
const totalWork = calcFormWorkMinutes(form);
|
const totalWork = calcFormWorkMinutes(form);
|
||||||
const totalProject = calcProjectMinutesTotal(projectLogs);
|
const totalProject = calcProjectMinutesTotal(
|
||||||
|
projectLogs.map((l) => ({
|
||||||
|
...l,
|
||||||
|
project_id:
|
||||||
|
l.project_id !== "" && l.project_id != null
|
||||||
|
? Number(l.project_id)
|
||||||
|
: undefined,
|
||||||
|
})),
|
||||||
|
);
|
||||||
const remaining = totalWork - totalProject;
|
const remaining = totalWork - totalProject;
|
||||||
const hasLogs = projectLogs.some((l) => l.project_id);
|
const hasLogs = projectLogs.some((l) => l.project_id);
|
||||||
|
|
||||||
@@ -329,7 +337,6 @@ export default function ShiftFormModal({
|
|||||||
<option value="work">Práce</option>
|
<option value="work">Práce</option>
|
||||||
<option value="vacation">Dovolená</option>
|
<option value="vacation">Dovolená</option>
|
||||||
<option value="sick">Nemoc</option>
|
<option value="sick">Nemoc</option>
|
||||||
<option value="holiday">Svátek</option>
|
|
||||||
<option value="unpaid">Neplacené volno</option>
|
<option value="unpaid">Neplacené volno</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -155,6 +155,26 @@ const menuSections: MenuSection[] = [
|
|||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/plan-work",
|
||||||
|
label: "Plán prací",
|
||||||
|
permission: ["attendance.manage", "attendance.record"],
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<rect x="3" y="4" width="18" height="18" rx="2" />
|
||||||
|
<line x1="16" y1="2" x2="16" y2="6" />
|
||||||
|
<line x1="8" y1="2" x2="8" y2="6" />
|
||||||
|
<line x1="3" y1="10" x2="21" y2="10" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -332,6 +352,201 @@ const menuSections: MenuSection[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Sklad",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
path: "/warehouse",
|
||||||
|
label: "Přehled",
|
||||||
|
permission: "warehouse.view",
|
||||||
|
end: true,
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
|
||||||
|
<polyline points="3.27 6.96 12 12.01 20.73 6.96" />
|
||||||
|
<line x1="12" y1="22.08" x2="12" y2="12" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/warehouse/items",
|
||||||
|
label: "Položky",
|
||||||
|
permission: "warehouse.view",
|
||||||
|
matchPrefix: "/warehouse/items",
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
|
||||||
|
<polyline points="3.27 6.96 12 12.01 20.73 6.96" />
|
||||||
|
<line x1="12" y1="22.08" x2="12" y2="12" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/warehouse/receipts",
|
||||||
|
label: "Příjmy",
|
||||||
|
permission: "warehouse.operate",
|
||||||
|
matchPrefix: "/warehouse/receipts",
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<polyline points="17 11 12 6 7 11" />
|
||||||
|
<line x1="12" y1="6" x2="12" y2="18" />
|
||||||
|
<path d="M5 19h14" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/warehouse/issues",
|
||||||
|
label: "Výdeje",
|
||||||
|
permission: "warehouse.operate",
|
||||||
|
matchPrefix: "/warehouse/issues",
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<polyline points="7 13 12 18 17 13" />
|
||||||
|
<line x1="12" y1="18" x2="12" y2="6" />
|
||||||
|
<path d="M5 5h14" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/warehouse/reservations",
|
||||||
|
label: "Rezervace",
|
||||||
|
permission: "warehouse.operate",
|
||||||
|
matchPrefix: "/warehouse/reservations",
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
|
||||||
|
<circle cx="9" cy="7" r="4" />
|
||||||
|
<line x1="19" y1="8" x2="19" y2="14" />
|
||||||
|
<line x1="22" y1="11" x2="16" y2="11" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/warehouse/inventory",
|
||||||
|
label: "Inventura",
|
||||||
|
permission: "warehouse.inventory",
|
||||||
|
matchPrefix: "/warehouse/inventory",
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M9 11l3 3L22 4" />
|
||||||
|
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/warehouse/reports",
|
||||||
|
label: "Reporty",
|
||||||
|
permission: "warehouse.view",
|
||||||
|
end: true,
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<line x1="18" y1="20" x2="18" y2="10" />
|
||||||
|
<line x1="12" y1="20" x2="12" y2="4" />
|
||||||
|
<line x1="6" y1="20" x2="6" y2="14" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/warehouse/categories",
|
||||||
|
label: "Kategorie",
|
||||||
|
permission: "warehouse.manage",
|
||||||
|
matchPrefix: "/warehouse/categories",
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<line x1="4" y1="21" x2="4" y2="14" />
|
||||||
|
<line x1="4" y1="10" x2="4" y2="3" />
|
||||||
|
<line x1="12" y1="21" x2="12" y2="12" />
|
||||||
|
<line x1="12" y1="8" x2="12" y2="3" />
|
||||||
|
<line x1="20" y1="21" x2="20" y2="16" />
|
||||||
|
<line x1="20" y1="12" x2="20" y2="3" />
|
||||||
|
<line x1="1" y1="14" x2="7" y2="14" />
|
||||||
|
<line x1="9" y1="8" x2="15" y2="8" />
|
||||||
|
<line x1="17" y1="16" x2="23" y2="16" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/warehouse/locations",
|
||||||
|
label: "Lokace",
|
||||||
|
permission: "warehouse.manage",
|
||||||
|
matchPrefix: "/warehouse/locations",
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<rect x="3" y="3" width="7" height="7" rx="1" />
|
||||||
|
<rect x="14" y="3" width="7" height="7" rx="1" />
|
||||||
|
<rect x="3" y="14" width="7" height="7" rx="1" />
|
||||||
|
<rect x="14" y="14" width="7" height="7" rx="1" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/warehouse/suppliers",
|
||||||
|
label: "Dodavatelé",
|
||||||
|
permission: "warehouse.manage",
|
||||||
|
matchPrefix: "/warehouse/suppliers",
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||||
|
<circle cx="9" cy="7" r="4" />
|
||||||
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
||||||
|
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Systém",
|
label: "Systém",
|
||||||
items: [
|
items: [
|
||||||
@@ -360,6 +575,7 @@ const menuSections: MenuSection[] = [
|
|||||||
"settings.company",
|
"settings.company",
|
||||||
"settings.banking",
|
"settings.banking",
|
||||||
"settings.roles",
|
"settings.roles",
|
||||||
|
"settings.system",
|
||||||
"settings.templates",
|
"settings.templates",
|
||||||
],
|
],
|
||||||
icon: (
|
icon: (
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import useModalLock from "../../hooks/useModalLock";
|
|||||||
import apiFetch from "../../utils/api";
|
import apiFetch from "../../utils/api";
|
||||||
import { formatSessionDate } from "../../utils/dashboardHelpers";
|
import { formatSessionDate } from "../../utils/dashboardHelpers";
|
||||||
import { sessionsOptions, type Session } from "../../lib/queries/dashboard";
|
import { sessionsOptions, type Session } from "../../lib/queries/dashboard";
|
||||||
import { Skeleton } from "boneyard-js/react";
|
|
||||||
import DashSessionsFixture from "../../fixtures/DashSessionsFixture";
|
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
@@ -154,11 +152,11 @@ export default function DashSessions() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-card-body" style={{ padding: 0 }}>
|
<div className="admin-card-body" style={{ padding: 0 }}>
|
||||||
<Skeleton
|
{sessionsLoading ? (
|
||||||
name="dash-sessions"
|
<div className="admin-loading">
|
||||||
loading={sessionsLoading}
|
<div className="admin-spinner" />
|
||||||
fixture={<DashSessionsFixture />}
|
</div>
|
||||||
>
|
) : (
|
||||||
<>
|
<>
|
||||||
{sessions.length === 0 && (
|
{sessions.length === 0 && (
|
||||||
<div
|
<div
|
||||||
@@ -231,7 +229,7 @@ export default function DashSessions() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
</Skeleton>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
|||||||
74
src/admin/components/warehouse/BatchPicker.tsx
Normal file
74
src/admin/components/warehouse/BatchPicker.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { jsonQuery } from "../../lib/apiAdapter";
|
||||||
|
|
||||||
|
interface Batch {
|
||||||
|
id: number;
|
||||||
|
quantity: number;
|
||||||
|
unit_price: number;
|
||||||
|
received_at: string;
|
||||||
|
is_consumed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BatchesResponse {
|
||||||
|
batches: Batch[];
|
||||||
|
total_stock: number;
|
||||||
|
reserved_quantity: number;
|
||||||
|
available_quantity: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BatchPickerProps {
|
||||||
|
itemId: number | null;
|
||||||
|
value: number | null;
|
||||||
|
onChange: (batchId: number, unitPrice: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BatchPicker({
|
||||||
|
itemId,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: BatchPickerProps) {
|
||||||
|
const { data: response } = useQuery({
|
||||||
|
queryKey: ["warehouse", "batches", itemId],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<BatchesResponse>(
|
||||||
|
`/api/admin/warehouse/items/${itemId}/batches`,
|
||||||
|
),
|
||||||
|
enabled: !!itemId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const batches = response?.batches ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{response && response.reserved_quantity > 0 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
color: "var(--text-tertiary)",
|
||||||
|
marginBottom: "0.25rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Dostupné: {response.available_quantity} ks (z toho rezervováno:{" "}
|
||||||
|
{response.reserved_quantity} ks)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<select
|
||||||
|
className="admin-form-select"
|
||||||
|
value={value ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const batchId = Number(e.target.value);
|
||||||
|
const batch = batches.find((b) => b.id === batchId);
|
||||||
|
if (batch) onChange(batch.id, Number(batch.unit_price));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">-- auto FIFO --</option>
|
||||||
|
{batches.map((b) => (
|
||||||
|
<option key={b.id} value={b.id}>
|
||||||
|
{new Date(b.received_at).toLocaleDateString("cs-CZ")} | {b.quantity}{" "}
|
||||||
|
ks | {Number(b.unit_price).toFixed(2)} Kč
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
228
src/admin/components/warehouse/ItemPicker.tsx
Normal file
228
src/admin/components/warehouse/ItemPicker.tsx
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
import { useState, useRef, useEffect, useCallback, useId } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { warehouseItemListOptions } from "../../lib/queries/warehouse";
|
||||||
|
|
||||||
|
interface ItemPickerProps {
|
||||||
|
value: number | null;
|
||||||
|
onChange: (itemId: number) => void;
|
||||||
|
itemName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ItemPicker({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
itemName,
|
||||||
|
}: ItemPickerProps) {
|
||||||
|
const [search, setSearch] = useState(itemName ?? "");
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const { data } = useQuery(warehouseItemListOptions({ search, perPage: 20 }));
|
||||||
|
const items = data?.data ?? [];
|
||||||
|
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const activeIndexRef = useRef<number>(-1);
|
||||||
|
const listboxId = useId();
|
||||||
|
const [activeIndex, setActiveIndex] = useState<number>(-1);
|
||||||
|
|
||||||
|
const [dropdownStyle, setDropdownStyle] = useState<{
|
||||||
|
position: "fixed";
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
width: number;
|
||||||
|
zIndex: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const updatePosition = useCallback(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
|
setDropdownStyle({
|
||||||
|
position: "fixed",
|
||||||
|
top: rect.bottom + 2,
|
||||||
|
left: rect.left,
|
||||||
|
width: rect.width,
|
||||||
|
zIndex: 100,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Reset active index when items change
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeIndex >= items.length) {
|
||||||
|
activeIndexRef.current = -1;
|
||||||
|
setActiveIndex(-1);
|
||||||
|
}
|
||||||
|
}, [items, activeIndex]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
updatePosition();
|
||||||
|
const onScroll = () => updatePosition();
|
||||||
|
const onClose = () => setOpen(false);
|
||||||
|
window.addEventListener("scroll", onScroll, true);
|
||||||
|
window.addEventListener("resize", onClose);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("scroll", onScroll, true);
|
||||||
|
window.removeEventListener("resize", onClose);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
setDropdownStyle(null);
|
||||||
|
activeIndexRef.current = -1;
|
||||||
|
setActiveIndex(-1);
|
||||||
|
}
|
||||||
|
}, [open, updatePosition]);
|
||||||
|
|
||||||
|
// Close on click-outside via mousedown on document
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
const onDocMouseDown = (e: MouseEvent) => {
|
||||||
|
const target = e.target;
|
||||||
|
if (containerRef.current && target instanceof Node) {
|
||||||
|
if (!containerRef.current.contains(target)) {
|
||||||
|
// Don't close if click landed on an option in the portal
|
||||||
|
const listEl = document.getElementById(listboxId);
|
||||||
|
if (listEl && listEl.contains(target)) return;
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", onDocMouseDown);
|
||||||
|
return () => document.removeEventListener("mousedown", onDocMouseDown);
|
||||||
|
}, [open, listboxId]);
|
||||||
|
|
||||||
|
const handleSelect = (itemId: number, name: string) => {
|
||||||
|
onChange(itemId);
|
||||||
|
setOpen(false);
|
||||||
|
setSearch(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setActive = (index: number) => {
|
||||||
|
activeIndexRef.current = index;
|
||||||
|
setActiveIndex(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === "ArrowDown") {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!open) {
|
||||||
|
setOpen(true);
|
||||||
|
if (items.length > 0) setActive(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const next =
|
||||||
|
activeIndexRef.current < items.length - 1
|
||||||
|
? activeIndexRef.current + 1
|
||||||
|
: 0;
|
||||||
|
setActive(next);
|
||||||
|
} else if (e.key === "ArrowUp") {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!open) {
|
||||||
|
setOpen(true);
|
||||||
|
if (items.length > 0) setActive(items.length - 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const prev =
|
||||||
|
activeIndexRef.current > 0
|
||||||
|
? activeIndexRef.current - 1
|
||||||
|
: items.length - 1;
|
||||||
|
setActive(prev);
|
||||||
|
} else if (e.key === "Enter") {
|
||||||
|
if (
|
||||||
|
open &&
|
||||||
|
activeIndexRef.current >= 0 &&
|
||||||
|
activeIndexRef.current < items.length
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
const item = items[activeIndexRef.current];
|
||||||
|
handleSelect(item.id, item.name);
|
||||||
|
}
|
||||||
|
} else if (e.key === "Escape") {
|
||||||
|
if (open) {
|
||||||
|
e.preventDefault();
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
} else if (e.key === "Home") {
|
||||||
|
if (open && items.length > 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
setActive(0);
|
||||||
|
}
|
||||||
|
} else if (e.key === "End") {
|
||||||
|
if (open && items.length > 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
setActive(items.length - 1);
|
||||||
|
}
|
||||||
|
} else if (e.key === "Tab") {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeId =
|
||||||
|
activeIndex >= 0 ? `${listboxId}-option-${activeIndex}` : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
style={{ position: "relative" }}
|
||||||
|
role="combobox"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-owns={listboxId}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="admin-form-input"
|
||||||
|
placeholder="Hledat položku..."
|
||||||
|
value={search}
|
||||||
|
role="searchbox"
|
||||||
|
aria-autocomplete="list"
|
||||||
|
aria-controls={listboxId}
|
||||||
|
aria-activedescendant={activeId}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSearch(e.target.value);
|
||||||
|
setOpen(true);
|
||||||
|
}}
|
||||||
|
onFocus={() => {
|
||||||
|
setOpen(true);
|
||||||
|
updatePosition();
|
||||||
|
}}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
/>
|
||||||
|
{open &&
|
||||||
|
items.length > 0 &&
|
||||||
|
dropdownStyle &&
|
||||||
|
createPortal(
|
||||||
|
<ul
|
||||||
|
id={listboxId}
|
||||||
|
role="listbox"
|
||||||
|
className="admin-item-picker-list"
|
||||||
|
style={dropdownStyle}
|
||||||
|
>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<li
|
||||||
|
key={item.id}
|
||||||
|
id={`${listboxId}-option-${index}`}
|
||||||
|
role="option"
|
||||||
|
aria-selected={value === item.id}
|
||||||
|
className={`admin-item-picker-item ${activeIndex === index ? "active" : ""}`}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSelect(item.id, item.name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="admin-item-picker-name">{item.name}</span>
|
||||||
|
{item.item_number && (
|
||||||
|
<span className="admin-item-picker-number">
|
||||||
|
{item.item_number}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{item.available_quantity !== undefined && (
|
||||||
|
<span className="admin-item-picker-qty">
|
||||||
|
{item.available_quantity} {item.unit}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
src/admin/components/warehouse/LocationSelect.tsx
Normal file
31
src/admin/components/warehouse/LocationSelect.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
warehouseLocationListOptions,
|
||||||
|
type WarehouseLocation,
|
||||||
|
} from "../../lib/queries/warehouse";
|
||||||
|
|
||||||
|
interface LocationSelectProps {
|
||||||
|
value: number | null;
|
||||||
|
onChange: (locationId: number | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LocationSelect({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: LocationSelectProps) {
|
||||||
|
const { data: locations } = useQuery(warehouseLocationListOptions());
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
className="admin-form-select"
|
||||||
|
value={value ?? ""}
|
||||||
|
onChange={(e) => onChange(e.target.value ? Number(e.target.value) : null)}
|
||||||
|
>
|
||||||
|
<option value="">-- bez lokace --</option>
|
||||||
|
{locations?.map((loc: WarehouseLocation) => (
|
||||||
|
<option key={loc.id} value={loc.id}>
|
||||||
|
{loc.code} - {loc.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
src/admin/components/warehouse/ReservationPicker.tsx
Normal file
54
src/admin/components/warehouse/ReservationPicker.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { warehouseReservationListOptions } from "../../lib/queries/warehouse";
|
||||||
|
|
||||||
|
interface ReservationPickerProps {
|
||||||
|
itemId: number | null;
|
||||||
|
projectId: number | null;
|
||||||
|
value: number | null;
|
||||||
|
onChange: (reservationId: number | null, remainingQty: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ReservationPicker({
|
||||||
|
itemId,
|
||||||
|
projectId,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: ReservationPickerProps) {
|
||||||
|
const { data: result } = useQuery(
|
||||||
|
warehouseReservationListOptions({
|
||||||
|
item_id: itemId ?? undefined,
|
||||||
|
project_id: projectId ?? undefined,
|
||||||
|
status: "ACTIVE",
|
||||||
|
perPage: 100,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const reservations = result?.data ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
className="admin-form-select"
|
||||||
|
value={value ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
if (!val) {
|
||||||
|
onChange(null, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const reservationId = Number(val);
|
||||||
|
const reservation = reservations.find((r) => r.id === reservationId);
|
||||||
|
if (reservation) {
|
||||||
|
onChange(reservationId, Number(reservation.remaining_qty));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">— žádná rezervace —</option>
|
||||||
|
{reservations.map((r) => (
|
||||||
|
<option key={r.id} value={r.id}>
|
||||||
|
R{r.id} — {r.project?.name ?? `Projekt #${r.project_id}`} — zbývá:{" "}
|
||||||
|
{Number(r.remaining_qty)} {r.item?.unit ?? "ks"}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
src/admin/components/warehouse/SupplierSelect.tsx
Normal file
33
src/admin/components/warehouse/SupplierSelect.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
warehouseSupplierListOptions,
|
||||||
|
type WarehouseSupplier,
|
||||||
|
} from "../../lib/queries/warehouse";
|
||||||
|
|
||||||
|
interface SupplierSelectProps {
|
||||||
|
value: number | null;
|
||||||
|
onChange: (supplierId: number | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SupplierSelect({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: SupplierSelectProps) {
|
||||||
|
const { data } = useQuery(warehouseSupplierListOptions({ perPage: 100 }));
|
||||||
|
const suppliers = data?.data ?? [];
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
className="admin-form-select"
|
||||||
|
value={value ?? ""}
|
||||||
|
onChange={(e) => onChange(e.target.value ? Number(e.target.value) : null)}
|
||||||
|
>
|
||||||
|
<option value="">-- bez dodavatele --</option>
|
||||||
|
{suppliers.map((s: WarehouseSupplier) => (
|
||||||
|
<option key={s.id} value={s.id}>
|
||||||
|
{s.name}
|
||||||
|
{s.ico ? ` (${s.ico})` : ""}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
||||||
249
src/admin/components/warehouse/WarehouseMovementTable.tsx
Normal file
249
src/admin/components/warehouse/WarehouseMovementTable.tsx
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
import { ReactNode, useId, useRef } from "react";
|
||||||
|
import ItemPicker from "./ItemPicker";
|
||||||
|
import LocationSelect from "./LocationSelect";
|
||||||
|
import BatchPicker from "./BatchPicker";
|
||||||
|
|
||||||
|
export interface MovementItem {
|
||||||
|
key: string;
|
||||||
|
item_id: number | null;
|
||||||
|
item_name?: string;
|
||||||
|
quantity: number;
|
||||||
|
unit_price: number;
|
||||||
|
location_id: number | null;
|
||||||
|
batch_id: number | null;
|
||||||
|
reservation_id: number | null;
|
||||||
|
notes: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MovementRowRenderer<T> = (
|
||||||
|
item: T,
|
||||||
|
index: number,
|
||||||
|
updateItem: (field: string, value: unknown) => void,
|
||||||
|
removeItem: () => void,
|
||||||
|
) => ReactNode;
|
||||||
|
|
||||||
|
interface WarehouseMovementTableProps<T extends { key: string }> {
|
||||||
|
items: T[];
|
||||||
|
onChange: (items: T[]) => void;
|
||||||
|
mode: "receipt" | "issue" | "inventory";
|
||||||
|
defaultItem?: () => Omit<T, "key">;
|
||||||
|
renderRow?: MovementRowRenderer<T>;
|
||||||
|
renderHeader?: () => ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDecimal(raw: string): number {
|
||||||
|
// Strip leading minus if you want negative support; here we want only positive
|
||||||
|
const cleaned = raw.replace(/[eE+\-]/g, "");
|
||||||
|
const n = Number(cleaned);
|
||||||
|
return Number.isFinite(n) ? n : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const builtInDefaultMovementItem = (): Omit<MovementItem, "key"> => ({
|
||||||
|
item_id: null,
|
||||||
|
quantity: 0,
|
||||||
|
unit_price: 0,
|
||||||
|
location_id: null,
|
||||||
|
batch_id: null,
|
||||||
|
reservation_id: null,
|
||||||
|
notes: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function WarehouseMovementTable<T extends { key: string }>({
|
||||||
|
items,
|
||||||
|
onChange,
|
||||||
|
mode,
|
||||||
|
defaultItem,
|
||||||
|
renderRow,
|
||||||
|
renderHeader,
|
||||||
|
}: WarehouseMovementTableProps<T>) {
|
||||||
|
const baseId = useId();
|
||||||
|
const counterRef = useRef(0);
|
||||||
|
const nextKey = () => `${baseId}-item-${counterRef.current++}`;
|
||||||
|
|
||||||
|
const addItem = () => {
|
||||||
|
const factory =
|
||||||
|
defaultItem ??
|
||||||
|
(builtInDefaultMovementItem as unknown as () => Omit<T, "key">);
|
||||||
|
onChange([...items, { ...(factory() as object), key: nextKey() } as T]);
|
||||||
|
};
|
||||||
|
const removeItem = (index: number) => {
|
||||||
|
onChange(items.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
const updateItem = (index: number, field: string, value: unknown) => {
|
||||||
|
const updated = [...items];
|
||||||
|
updated[index] = { ...updated[index], [field]: value };
|
||||||
|
onChange(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
// If a custom row renderer is supplied, the caller is fully in charge of
|
||||||
|
// the row layout (and typically the column headers too — see inventory mode).
|
||||||
|
if (renderRow) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-warehouse-movement-table">
|
||||||
|
<table className="admin-table">
|
||||||
|
{renderHeader && (
|
||||||
|
<thead>
|
||||||
|
<tr>{renderHeader()}</tr>
|
||||||
|
</thead>
|
||||||
|
)}
|
||||||
|
<tbody>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<tr key={item.key}>
|
||||||
|
{renderRow(
|
||||||
|
item,
|
||||||
|
index,
|
||||||
|
(field, value) => updateItem(index, field, value),
|
||||||
|
() => removeItem(index),
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="admin-btn admin-btn-secondary"
|
||||||
|
onClick={addItem}
|
||||||
|
>
|
||||||
|
+ Přidat řádek
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default rendering: only valid for "receipt" | "issue" — narrow at runtime.
|
||||||
|
const movementItems = items as unknown as MovementItem[];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-warehouse-movement-table">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="admin-warehouse-col-item">Položka</th>
|
||||||
|
{mode === "issue" && (
|
||||||
|
<th className="admin-warehouse-col-batch">Šarže</th>
|
||||||
|
)}
|
||||||
|
<th className="admin-warehouse-col-qty">Množství</th>
|
||||||
|
<th className="admin-warehouse-col-price">Cena/ks</th>
|
||||||
|
<th className="admin-warehouse-col-location">Lokace</th>
|
||||||
|
<th className="admin-warehouse-col-notes">Poznámka</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{movementItems.map((item, index) => (
|
||||||
|
<tr key={item.key}>
|
||||||
|
<td className="admin-warehouse-col-item">
|
||||||
|
<ItemPicker
|
||||||
|
value={item.item_id}
|
||||||
|
itemName={item.item_name}
|
||||||
|
onChange={(id) => updateItem(index, "item_id", id)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
{mode === "issue" && (
|
||||||
|
<td className="admin-warehouse-col-batch">
|
||||||
|
<BatchPicker
|
||||||
|
itemId={item.item_id}
|
||||||
|
value={item.batch_id}
|
||||||
|
onChange={(batchId, unitPrice) => {
|
||||||
|
updateItem(index, "batch_id", batchId);
|
||||||
|
updateItem(index, "unit_price", unitPrice);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td className="admin-warehouse-col-qty">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="admin-form-input"
|
||||||
|
value={item.quantity || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateItem(
|
||||||
|
index,
|
||||||
|
"quantity",
|
||||||
|
parseDecimal(e.target.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
min="0"
|
||||||
|
step="0.001"
|
||||||
|
inputMode="decimal"
|
||||||
|
pattern="[0-9]+([\.,][0-9]+)?"
|
||||||
|
aria-label={`Množství, řádek ${index + 1}`}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="admin-warehouse-col-price">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="admin-form-input"
|
||||||
|
value={item.unit_price || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateItem(
|
||||||
|
index,
|
||||||
|
"unit_price",
|
||||||
|
parseDecimal(e.target.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
disabled={mode === "issue"}
|
||||||
|
inputMode="decimal"
|
||||||
|
pattern="[0-9]+([\.,][0-9]+)?"
|
||||||
|
aria-label={`Cena za kus, řádek ${index + 1}`}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="admin-warehouse-col-location">
|
||||||
|
<LocationSelect
|
||||||
|
value={item.location_id}
|
||||||
|
onChange={(id) => updateItem(index, "location_id", id)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="admin-warehouse-col-notes">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="admin-form-input"
|
||||||
|
value={item.notes ?? ""}
|
||||||
|
onChange={(e) => updateItem(index, "notes", e.target.value)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="admin-btn-icon danger"
|
||||||
|
onClick={() => removeItem(index)}
|
||||||
|
title="Odebrat řádek"
|
||||||
|
aria-label="Odebrat řádek"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="admin-btn admin-btn-secondary"
|
||||||
|
onClick={addItem}
|
||||||
|
>
|
||||||
|
+ Přidat řádek
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
export default function AttendanceAdminFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Správa docházky</h1>
|
|
||||||
</div>
|
|
||||||
<div className="admin-page-actions">
|
|
||||||
<button className="admin-btn admin-btn-secondary">
|
|
||||||
Vyplnit měsíc
|
|
||||||
</button>
|
|
||||||
<button className="admin-btn admin-btn-primary">Přidat záznam</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card mb-6">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-form-row">
|
|
||||||
<label className="admin-form-label">Měsíc</label>
|
|
||||||
<select className="admin-form-select" />
|
|
||||||
<label className="admin-form-label">Zaměstnanec</label>
|
|
||||||
<select className="admin-form-select" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-grid admin-grid-3">
|
|
||||||
{Array.from({ length: 3 }, (_, i) => (
|
|
||||||
<div key={i} className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="flex-row gap-2 mb-2">
|
|
||||||
<span style={{ fontWeight: 600 }}>Jan Novák</span>
|
|
||||||
<span className="attendance-working-badge finished">
|
|
||||||
✗
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="admin-stat-value">8:00</div>
|
|
||||||
<div className="admin-stat-label">odpracováno</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th>Příchod</th>
|
|
||||||
<th>Odchod</th>
|
|
||||||
<th>Hodiny</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 4 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>1. 4. 2025</td>
|
|
||||||
<td className="admin-mono">08:00</td>
|
|
||||||
<td className="admin-mono">16:00</td>
|
|
||||||
<td className="admin-mono">8:00</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
export default function AttendanceBalancesFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Správa bilancí</h1>
|
|
||||||
</div>
|
|
||||||
<div className="admin-page-actions">
|
|
||||||
<select className="admin-form-select" style={{ minWidth: 100 }}>
|
|
||||||
<option>2025</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Zaměstnanec</th>
|
|
||||||
<th>Nárok (h)</th>
|
|
||||||
<th>Čerpáno (h)</th>
|
|
||||||
<th>Zbývá (h)</th>
|
|
||||||
<th>Nemoc (h)</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 4 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="fw-500">Jan Novák</td>
|
|
||||||
<td className="admin-mono">160</td>
|
|
||||||
<td className="admin-mono">40.0</td>
|
|
||||||
<td className="admin-mono">120.0</td>
|
|
||||||
<td className="admin-mono">8.0</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn-icon" title="Upravit">
|
|
||||||
✎
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-6">
|
|
||||||
<h2 className="admin-page-title mb-4" style={{ fontSize: "1.25rem" }}>
|
|
||||||
Měsíční přehled fondu 2025
|
|
||||||
</h2>
|
|
||||||
<div className="admin-grid admin-grid-3">
|
|
||||||
{Array.from({ length: 3 }, (_, i) => (
|
|
||||||
<div key={i} className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<h3 style={{ fontWeight: 600, fontSize: "1rem", margin: 0 }}>
|
|
||||||
Duben
|
|
||||||
</h3>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "0.375rem",
|
|
||||||
marginTop: "0.5rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
fontSize: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>Jan Novák</span>
|
|
||||||
<span className="text-secondary">8h</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
height: 3,
|
|
||||||
background: "var(--bg-tertiary)",
|
|
||||||
borderRadius: 2,
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
height: "100%",
|
|
||||||
width: "75%",
|
|
||||||
background: "var(--gradient)",
|
|
||||||
borderRadius: 2,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
export default function AttendanceCreateFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<h1 className="admin-page-title">Zapsat docházku</h1>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card" style={{ maxWidth: 600 }}>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<FormField label="Uživatel">
|
|
||||||
<select className="admin-form-select">
|
|
||||||
<option>Jan Novák</option>
|
|
||||||
</select>
|
|
||||||
</FormField>
|
|
||||||
<FormField label="Datum">
|
|
||||||
<input type="date" className="admin-form-input" />
|
|
||||||
</FormField>
|
|
||||||
<FormField label="Příchod">
|
|
||||||
<input type="time" className="admin-form-input" />
|
|
||||||
</FormField>
|
|
||||||
<FormField label="Odchod">
|
|
||||||
<input type="time" className="admin-form-input" />
|
|
||||||
</FormField>
|
|
||||||
<button className="admin-btn admin-btn-primary">Uložit</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FormField({
|
|
||||||
label,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">{label}</label>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
export default function AttendanceFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Docházka</h1>
|
|
||||||
<p className="admin-page-subtitle">pondělí 28. dubna 2025</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="attendance-layout">
|
|
||||||
<div className="attendance-main">
|
|
||||||
<div className="attendance-clock-card">
|
|
||||||
<div className="attendance-clock-header">
|
|
||||||
<div className="attendance-clock-status">
|
|
||||||
<span className="attendance-status-dot" />
|
|
||||||
<span>Nepracuji</span>
|
|
||||||
</div>
|
|
||||||
<div className="attendance-clock-time">08:30</div>
|
|
||||||
</div>
|
|
||||||
<div className="attendance-clock-actions">
|
|
||||||
<button className="admin-btn admin-btn-primary w-full">
|
|
||||||
Příchod
|
|
||||||
</button>
|
|
||||||
<button className="admin-btn admin-btn-secondary w-full">
|
|
||||||
Žádost o nepřítomnost
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="attendance-sidebar">
|
|
||||||
<div className="attendance-balance-card">
|
|
||||||
<h3 className="attendance-balance-title">Dovolená 2025</h3>
|
|
||||||
<div className="attendance-balance-value">
|
|
||||||
<span className="attendance-balance-number">12</span>
|
|
||||||
<span className="attendance-balance-unit">dnů</span>
|
|
||||||
</div>
|
|
||||||
<div className="attendance-balance-detail">
|
|
||||||
<span>Celkem: 160h</span>
|
|
||||||
<span>Čerpáno: 64h</span>
|
|
||||||
</div>
|
|
||||||
<div className="attendance-balance-bar">
|
|
||||||
<div
|
|
||||||
className="attendance-balance-progress"
|
|
||||||
style={{ width: "60%" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-stat-card">
|
|
||||||
<div className="admin-stat-icon danger">
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="admin-stat-content">
|
|
||||||
<span className="admin-stat-label">Nemoc 2025</span>
|
|
||||||
<span className="admin-stat-value">16h čerpáno</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="attendance-quick-links">
|
|
||||||
<h4 className="attendance-quick-title">Rychlé odkazy</h4>
|
|
||||||
<a className="attendance-quick-link">
|
|
||||||
<span>Moje žádosti</span>
|
|
||||||
</a>
|
|
||||||
<a className="attendance-quick-link">
|
|
||||||
<span>Historie docházky</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
export default function AttendanceHistoryFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Historie docházky</h1>
|
|
||||||
<p className="admin-page-subtitle">duben 2025</p>
|
|
||||||
</div>
|
|
||||||
<div className="admin-page-actions">
|
|
||||||
<button className="admin-btn admin-btn-secondary">Tisk</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card mb-6">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-form-row">
|
|
||||||
<label className="admin-form-label">Měsíc</label>
|
|
||||||
<input className="admin-form-input" readOnly value="04/2025" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card mb-6">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
|
|
||||||
<div className="admin-stat-icon info">
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
|
|
||||||
<line x1="16" y1="2" x2="16" y2="6" />
|
|
||||||
<line x1="8" y1="2" x2="8" y2="6" />
|
|
||||||
<line x1="3" y1="10" x2="21" y2="10" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div style={{ flex: 1, minWidth: 200 }}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "baseline",
|
|
||||||
marginBottom: "0.375rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span style={{ fontWeight: 600 }}>Fond: 120h / 160h</span>
|
|
||||||
<span
|
|
||||||
className="text-secondary"
|
|
||||||
style={{ fontSize: "0.8125rem" }}
|
|
||||||
>
|
|
||||||
20 prac. dnů
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="attendance-balance-bar">
|
|
||||||
<div
|
|
||||||
className="attendance-balance-progress"
|
|
||||||
style={{ width: "75%" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th>Typ</th>
|
|
||||||
<th>Příchod</th>
|
|
||||||
<th>Pauza</th>
|
|
||||||
<th>Odchod</th>
|
|
||||||
<th>Hodiny</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="admin-mono">1. 4. 2025</td>
|
|
||||||
<td>
|
|
||||||
<span className="admin-badge admin-badge-info">
|
|
||||||
Práce
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="admin-mono">08:00</td>
|
|
||||||
<td className="admin-mono">12:00 – 12:30</td>
|
|
||||||
<td className="admin-mono">16:30</td>
|
|
||||||
<td className="admin-mono">8:00</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
export default function AttendanceLocationFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}>
|
|
||||||
<svg
|
|
||||||
width="32"
|
|
||||||
height="32"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
|
|
||||||
<circle cx="12" cy="10" r="3" />
|
|
||||||
</svg>
|
|
||||||
<h1 className="admin-page-title">Lokace</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card" style={{ height: 300, marginBottom: "1rem" }}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
background: "var(--bg-secondary)",
|
|
||||||
height: "100%",
|
|
||||||
borderRadius: 8,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem" }}
|
|
||||||
>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<h3 style={{ marginBottom: "0.5rem" }}>Poloha</h3>
|
|
||||||
<p>50.0755° N, 14.4378° E</p>
|
|
||||||
<p>Praha, Česká republika</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<h3 style={{ marginBottom: "0.5rem" }}>Čas záznamu</h3>
|
|
||||||
<p>1. 1. 2024 08:00</p>
|
|
||||||
<p>Přesnost: 10 m</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
export default function AuditLogFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Audit log</h1>
|
|
||||||
<p className="admin-page-subtitle">Záznam změn v systému</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div
|
|
||||||
className="admin-search-bar mb-4"
|
|
||||||
style={{ display: "flex", gap: "0.5rem" }}
|
|
||||||
>
|
|
||||||
<input className="admin-form-input" placeholder="" />
|
|
||||||
<select className="admin-form-select" />
|
|
||||||
<select className="admin-form-select" />
|
|
||||||
</div>
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Čas</th>
|
|
||||||
<th>Uživatel</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
<th>Entita</th>
|
|
||||||
<th>Detail</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="admin-mono">1. 1. 2024 10:00</td>
|
|
||||||
<td>admin</td>
|
|
||||||
<td>
|
|
||||||
<span className="admin-badge admin-badge-create">
|
|
||||||
Vytvoření
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>Faktura</td>
|
|
||||||
<td style={{ maxWidth: 300 }}>Nová faktura FV-2024-001</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
export default function CompanySettingsFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Nastavení firmy</h1>
|
|
||||||
<p className="admin-page-subtitle">Firemní údaje a bankovní účty</p>
|
|
||||||
</div>
|
|
||||||
<button className="admin-btn admin-btn-primary">
|
|
||||||
Uložit nastavení
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="admin-settings-grid">
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-header">
|
|
||||||
<h3 className="admin-card-title">Firemní údaje</h3>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-form">
|
|
||||||
<label className="admin-form-label">Název firmy</label>
|
|
||||||
<input
|
|
||||||
className="admin-form-input"
|
|
||||||
readOnly
|
|
||||||
value="BOHA s.r.o."
|
|
||||||
/>
|
|
||||||
<div className="admin-form-row">
|
|
||||||
<label className="admin-form-label">Ulice</label>
|
|
||||||
<input
|
|
||||||
className="admin-form-input"
|
|
||||||
readOnly
|
|
||||||
value="Hlavní 123"
|
|
||||||
/>
|
|
||||||
<label className="admin-form-label">Město</label>
|
|
||||||
<input className="admin-form-input" readOnly value="Praha" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-header">
|
|
||||||
<h3 className="admin-card-title">Bankovní účty</h3>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Název</th>
|
|
||||||
<th>Banka</th>
|
|
||||||
<th>Číslo účtu</th>
|
|
||||||
<th>Měna</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Hlavní účet</td>
|
|
||||||
<td>ČSOB</td>
|
|
||||||
<td className="admin-mono">123456/0300</td>
|
|
||||||
<td>CZK</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
export default function DashSessionsFixture() {
|
|
||||||
return (
|
|
||||||
<div className="admin-card">
|
|
||||||
<div
|
|
||||||
className="admin-card-header"
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
gap: "0.75rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h2 className="admin-card-title">Přihlášená zařízení</h2>
|
|
||||||
<button className="admin-btn admin-btn-secondary admin-btn-sm">
|
|
||||||
Odhlásit ostatní
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card-body" style={{ padding: 0 }}>
|
|
||||||
<div className="dash-sessions-list">
|
|
||||||
{Array.from({ length: 3 }, (_, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className={`dash-session-item${i === 0 ? " dash-session-item-current" : ""}`}
|
|
||||||
>
|
|
||||||
<div className="dash-session-icon">
|
|
||||||
<svg
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
|
|
||||||
<line x1="8" y1="21" x2="16" y2="21" />
|
|
||||||
<line x1="12" y1="17" x2="12" y2="21" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="dash-session-info">
|
|
||||||
<div className="dash-session-device">
|
|
||||||
Chrome na Windows
|
|
||||||
{i === 0 && (
|
|
||||||
<span
|
|
||||||
className="admin-badge admin-badge-success"
|
|
||||||
style={{ marginLeft: "0.5rem" }}
|
|
||||||
>
|
|
||||||
Aktuální
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="dash-session-meta">
|
|
||||||
<span>192.168.1.100</span>
|
|
||||||
<span className="dash-session-meta-separator">|</span>
|
|
||||||
<span>před 2 hodinami</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
export default function DashboardFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Dashboard</h1>
|
|
||||||
<p className="admin-page-subtitle">Přehled</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="dash-kpi-grid"
|
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "repeat(4, 1fr)",
|
|
||||||
gap: "1rem",
|
|
||||||
marginBottom: "1rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{["Nabídky", "Objednávky", "Faktury", "Projekty"].map((label, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="dash-kpi-card"
|
|
||||||
style={{
|
|
||||||
padding: "1.25rem",
|
|
||||||
borderRadius: 10,
|
|
||||||
background: "var(--bg-secondary)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ fontSize: "0.875rem", marginBottom: "0.25rem" }}>
|
|
||||||
{label}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: "1.5rem", fontWeight: 600 }}>12</div>
|
|
||||||
<div style={{ fontSize: "0.75rem" }}>tento měsíc</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="dash-quick-actions"
|
|
||||||
style={{ display: "flex", gap: "0.5rem", marginBottom: "1rem" }}
|
|
||||||
>
|
|
||||||
{["Nová nabídka", "Nová faktura", "Zapsat docházku"].map((label, i) => (
|
|
||||||
<button
|
|
||||||
key={i}
|
|
||||||
className="admin-btn admin-btn-secondary"
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "2fr 1fr",
|
|
||||||
gap: "1rem",
|
|
||||||
marginBottom: "1rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="admin-card" style={{ minHeight: 320 }}>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<h3 className="admin-card-title">Docházka dnes</h3>
|
|
||||||
<div style={{ height: 200 }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
||||||
<div className="admin-card" style={{ minHeight: 150 }}>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<h3 className="admin-card-title">Aktivita</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card" style={{ minHeight: 150 }}>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<h3 className="admin-card-title">Profil</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem" }}
|
|
||||||
>
|
|
||||||
<div className="admin-card" style={{ minHeight: 200 }}>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<h3 className="admin-card-title">Relace</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card" style={{ minHeight: 200 }}>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<h3 className="admin-card-title">Poslední aktivity</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
export default function InvoiceDetailFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
|
||||||
<a href="/invoices" className="admin-btn-icon">
|
|
||||||
<svg
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<path d="M19 12H5M12 19l-7-7 7-7" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">FV-2024-001</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-page-actions">
|
|
||||||
<button className="admin-btn admin-btn-primary">Uložit</button>
|
|
||||||
<button className="admin-btn admin-btn-secondary">Zaplaceno</button>
|
|
||||||
<button className="admin-btn admin-btn-secondary">Smazat</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card" style={{ marginBottom: "1rem" }}>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div
|
|
||||||
className="admin-form-row"
|
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "1fr 1fr",
|
|
||||||
gap: "1rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Zákazník</label>
|
|
||||||
<div>Firma s.r.o.</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Stav</label>
|
|
||||||
<span className="admin-badge admin-badge-invoice-issued">
|
|
||||||
Vystavena
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Datum vystavení</label>
|
|
||||||
<div>1. 1. 2024</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Datum splatnosti</label>
|
|
||||||
<div>15. 1. 2024</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<h3 className="admin-card-title">Položky</h3>
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Položka</th>
|
|
||||||
<th>Množství</th>
|
|
||||||
<th>Cena</th>
|
|
||||||
<th>Celkem</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 3 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>Služba {i + 1}</td>
|
|
||||||
<td>1</td>
|
|
||||||
<td>10 000 Kč</td>
|
|
||||||
<td>10 000 Kč</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
export default function InvoicesFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Faktury</h1>
|
|
||||||
<p className="admin-page-subtitle">15 faktur</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="dash-kpi-grid"
|
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "repeat(4, 1fr)",
|
|
||||||
gap: "1rem",
|
|
||||||
marginBottom: "1rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{["Vystaveno", "Zaplaceno", "Po splatnosti", "Celkem"].map(
|
|
||||||
(label, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="dash-kpi-card"
|
|
||||||
style={{
|
|
||||||
padding: "1.25rem",
|
|
||||||
borderRadius: 10,
|
|
||||||
background: "var(--bg-secondary)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ fontSize: "0.875rem", marginBottom: "0.25rem" }}>
|
|
||||||
{label}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: "1.5rem", fontWeight: 600 }}>
|
|
||||||
{i * 5 + 3}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-search-bar mb-4">
|
|
||||||
<input className="admin-form-input" placeholder="" />
|
|
||||||
</div>
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Číslo</th>
|
|
||||||
<th>Zákazník</th>
|
|
||||||
<th>Stav</th>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th className="text-right">Částka</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="admin-mono">FV-2024-00{i + 1}</td>
|
|
||||||
<td>Firma s.r.o.</td>
|
|
||||||
<td>
|
|
||||||
<span className="admin-badge admin-badge-invoice-issued">
|
|
||||||
Vystaveno
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="admin-mono">1. 1. 2024</td>
|
|
||||||
<td className="admin-mono text-right">50 000 Kč</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn-icon">👁</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
export default function LeaveApprovalFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Schvalování dovolené</h1>
|
|
||||||
<p className="admin-page-subtitle">2 čekající</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Uživatel</th>
|
|
||||||
<th>Typ</th>
|
|
||||||
<th>Od</th>
|
|
||||||
<th>Do</th>
|
|
||||||
<th>Dní</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 3 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>Jan Novák</td>
|
|
||||||
<td>Dovolená</td>
|
|
||||||
<td className="admin-mono">1. 7. 2024</td>
|
|
||||||
<td className="admin-mono">5. 7. 2024</td>
|
|
||||||
<td>5</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn admin-btn-sm admin-btn-primary">
|
|
||||||
Schválit
|
|
||||||
</button>
|
|
||||||
<button className="admin-btn admin-btn-sm admin-btn-secondary">
|
|
||||||
Zamítnout
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
export default function LeaveRequestsFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Žádosti o dovolenou</h1>
|
|
||||||
<p className="admin-page-subtitle">3 žádosti</p>
|
|
||||||
</div>
|
|
||||||
<button className="admin-btn admin-btn-primary">+ Nová žádost</button>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Uživatel</th>
|
|
||||||
<th>Typ</th>
|
|
||||||
<th>Od</th>
|
|
||||||
<th>Do</th>
|
|
||||||
<th>Dní</th>
|
|
||||||
<th>Stav</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 3 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>Jan Novák</td>
|
|
||||||
<td>Dovolená</td>
|
|
||||||
<td className="admin-mono">1. 7. 2024</td>
|
|
||||||
<td className="admin-mono">5. 7. 2024</td>
|
|
||||||
<td>5</td>
|
|
||||||
<td>
|
|
||||||
<span className="admin-badge admin-badge-pending">
|
|
||||||
Čeká
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn-icon">Zrušit</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
export default function OfferDetailFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<button className="admin-btn-icon">←</button>
|
|
||||||
<h1 className="admin-page-title">NAB-2024-001</h1>
|
|
||||||
<button className="admin-btn admin-btn-primary">Uložit</button>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div
|
|
||||||
className="admin-form-row"
|
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "1fr 1fr",
|
|
||||||
gap: "1rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Číslo nabídky</label>
|
|
||||||
<div className="admin-form-input">NAB-2024-001</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Zákazník</label>
|
|
||||||
<div className="admin-form-input">Firma s.r.o.</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Datum</label>
|
|
||||||
<div className="admin-form-input">1. 1. 2024</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Platnost do</label>
|
|
||||||
<div className="admin-form-input">31. 1. 2024</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<table className="admin-table" style={{ marginTop: "1rem" }}>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Položka</th>
|
|
||||||
<th>Množství</th>
|
|
||||||
<th>Cena</th>
|
|
||||||
<th>Celkem</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 3 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>Položka {i + 1}</td>
|
|
||||||
<td>1</td>
|
|
||||||
<td>10 000 Kč</td>
|
|
||||||
<td>10 000 Kč</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
export default function OffersCustomersFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Zákazníci</h1>
|
|
||||||
<p className="admin-page-subtitle">8 zákazníků</p>
|
|
||||||
</div>
|
|
||||||
<button className="admin-btn admin-btn-primary">
|
|
||||||
+ Přidat zákazníka
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-search-bar mb-4">
|
|
||||||
<input className="admin-form-input" placeholder="" />
|
|
||||||
</div>
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Název</th>
|
|
||||||
<th>Město</th>
|
|
||||||
<th>IČO</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>Firma s.r.o.</td>
|
|
||||||
<td>Praha</td>
|
|
||||||
<td className="admin-mono">12345678</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn-icon">✎</button>
|
|
||||||
<button className="admin-btn-icon danger">🗑</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
export default function OffersFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Nabídky</h1>
|
|
||||||
<p className="admin-page-subtitle">12 nabídek</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-search-bar mb-4">
|
|
||||||
<input className="admin-form-input" placeholder="" />
|
|
||||||
</div>
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Číslo</th>
|
|
||||||
<th>Zákazník</th>
|
|
||||||
<th>Stav</th>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th className="text-right">Celkem</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="admin-mono">NAB-2024-00{i + 1}</td>
|
|
||||||
<td>Firma s.r.o.</td>
|
|
||||||
<td>
|
|
||||||
<span className="admin-badge admin-badge-offer-active">
|
|
||||||
Aktivní
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="admin-mono">1. 1. 2024</td>
|
|
||||||
<td className="admin-mono text-right fw-500">100 000 Kč</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn-icon">👁</button>
|
|
||||||
<button className="admin-btn-icon">✎</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
export default function OffersTemplatesFixture() {
|
|
||||||
return (
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-search-bar mb-4">
|
|
||||||
<input className="admin-form-input" placeholder="" />
|
|
||||||
</div>
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Název</th>
|
|
||||||
<th>Cena</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>Šablona {i + 1}</td>
|
|
||||||
<td className="admin-mono text-right">1 000 Kč</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn-icon">✎</button>
|
|
||||||
<button className="admin-btn-icon danger">🗑</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
export default function OrderDetailFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div className="admin-page-header-left">
|
|
||||||
<Link
|
|
||||||
to="/orders"
|
|
||||||
className="admin-btn-icon"
|
|
||||||
style={{ marginRight: "0.5rem" }}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<path d="M19 12H5M12 19l-7-7 7-7" />
|
|
||||||
</svg>
|
|
||||||
</Link>
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">OBJ-2024-001</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-page-actions">
|
|
||||||
<button className="admin-btn admin-btn-primary">Uložit</button>
|
|
||||||
<button className="admin-btn admin-btn-secondary">Stornovat</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card" style={{ marginBottom: "1rem" }}>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div
|
|
||||||
className="admin-form-row"
|
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "1fr 1fr",
|
|
||||||
gap: "1rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Zákazník</label>
|
|
||||||
<div>Firma s.r.o.</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Stav</label>
|
|
||||||
<span className="admin-badge admin-badge-order-realizace">
|
|
||||||
V realizaci
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Datum vytvoření</label>
|
|
||||||
<div>1. 1. 2024</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Celkem</label>
|
|
||||||
<div className="fw-500">50 000 Kč</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<h3 className="admin-card-title">Položky</h3>
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Položka</th>
|
|
||||||
<th>Množství</th>
|
|
||||||
<th>Cena</th>
|
|
||||||
<th>Celkem</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 3 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>Položka {i + 1}</td>
|
|
||||||
<td>1</td>
|
|
||||||
<td>10 000 Kč</td>
|
|
||||||
<td>10 000 Kč</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
export default function OrdersFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Objednávky</h1>
|
|
||||||
<p className="admin-page-subtitle">8 objednávek</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-search-bar mb-4">
|
|
||||||
<input className="admin-form-input" placeholder="" />
|
|
||||||
</div>
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Číslo</th>
|
|
||||||
<th>Nabídka</th>
|
|
||||||
<th>Zákazník</th>
|
|
||||||
<th>Stav</th>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th className="text-right">Celkem</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="admin-mono">OBJ-2024-00{i + 1}</td>
|
|
||||||
<td>NAB-2024-00{i + 1}</td>
|
|
||||||
<td>Firma s.r.o.</td>
|
|
||||||
<td>
|
|
||||||
<span className="admin-badge admin-badge-order-realizace">
|
|
||||||
V realizaci
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="admin-mono">1. 1. 2024</td>
|
|
||||||
<td className="admin-mono text-right fw-500">50 000 Kč</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn-icon">👁</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
export default function ProjectDetailFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
|
||||||
<a href="/projects" className="admin-btn-icon">
|
|
||||||
<svg
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<path d="M19 12H5M12 19l-7-7 7-7" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">PRJ-001 Projekt Alpha</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-page-actions">
|
|
||||||
<button className="admin-btn admin-btn-primary">Uložit</button>
|
|
||||||
<button className="admin-btn admin-btn-secondary">Smazat</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card" style={{ marginBottom: "1rem" }}>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div
|
|
||||||
className="admin-form-row"
|
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "1fr 1fr",
|
|
||||||
gap: "1rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Název projektu</label>
|
|
||||||
<input
|
|
||||||
className="admin-form-input"
|
|
||||||
value="Projekt Alpha"
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Stav</label>
|
|
||||||
<select className="admin-form-select">
|
|
||||||
<option>Aktivní</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Začátek</label>
|
|
||||||
<input type="date" className="admin-form-input" readOnly />
|
|
||||||
</div>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Konec</label>
|
|
||||||
<input type="date" className="admin-form-input" readOnly />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card" style={{ marginBottom: "1rem" }}>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<h3 className="admin-card-title">Poznámky</h3>
|
|
||||||
<textarea className="admin-form-input" rows={4} readOnly />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
export default function ProjectFileManagerFixture() {
|
|
||||||
return (
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<h3 className="admin-card-title">Soubory</h3>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "0.5rem",
|
|
||||||
marginBottom: "0.75rem",
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
color: "var(--text-secondary)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>Projekt</span>
|
|
||||||
<span>/</span>
|
|
||||||
<span>Dokumentace</span>
|
|
||||||
</div>
|
|
||||||
{Array.from({ length: 4 }, (_, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "0.5rem",
|
|
||||||
padding: "0.5rem 0",
|
|
||||||
borderBottom: i < 3 ? "1px solid var(--border-color)" : "none",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
>
|
|
||||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
||||||
<polyline points="14 2 14 8 20 8" />
|
|
||||||
</svg>
|
|
||||||
<span style={{ flex: 1 }}>dokument_{i + 1}.pdf</span>
|
|
||||||
<span
|
|
||||||
style={{ color: "var(--text-secondary)", fontSize: "0.8rem" }}
|
|
||||||
>
|
|
||||||
2.{i + 1} MB
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
export default function ProjectsFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Projekty</h1>
|
|
||||||
<p className="admin-page-subtitle">6 projektů</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-search-bar mb-4">
|
|
||||||
<input className="admin-form-input" placeholder="" />
|
|
||||||
</div>
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Číslo</th>
|
|
||||||
<th>Název</th>
|
|
||||||
<th>Zákazník</th>
|
|
||||||
<th>Stav</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="admin-mono">PRJ-2024-00{i + 1}</td>
|
|
||||||
<td>Projekt Alpha</td>
|
|
||||||
<td>Firma s.r.o.</td>
|
|
||||||
<td>
|
|
||||||
<span className="admin-badge admin-badge-project-active">
|
|
||||||
Aktivní
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn-icon">👁</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
export default function ReceivedInvoicesFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Přijaté faktury</h1>
|
|
||||||
<p className="admin-page-subtitle">8 faktur</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="dash-kpi-grid"
|
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "repeat(4, 1fr)",
|
|
||||||
gap: "1rem",
|
|
||||||
marginBottom: "1rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{["K úhradě", "Zaplaceno", "Po splatnosti", "Celkem"].map(
|
|
||||||
(label, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="dash-kpi-card"
|
|
||||||
style={{
|
|
||||||
padding: "1.25rem",
|
|
||||||
borderRadius: 10,
|
|
||||||
background: "var(--bg-secondary)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ fontSize: "0.875rem", marginBottom: "0.25rem" }}>
|
|
||||||
{label}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: "1.5rem", fontWeight: 600 }}>
|
|
||||||
{i * 3 + 2}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Číslo</th>
|
|
||||||
<th>Dodavatel</th>
|
|
||||||
<th>Částka</th>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th>Stav</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="admin-mono">PF-2024-00{i + 1}</td>
|
|
||||||
<td>Dodavatel s.r.o.</td>
|
|
||||||
<td className="admin-mono text-right">10 000 Kč</td>
|
|
||||||
<td className="admin-mono">1. 1. 2024</td>
|
|
||||||
<td>
|
|
||||||
<span className="admin-badge admin-badge-invoice-issued">
|
|
||||||
K úhradě
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn-icon">✎</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
export default function SettingsFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Nastavení</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-tab-bar">
|
|
||||||
<button className="admin-tab active">Role</button>
|
|
||||||
<button className="admin-tab">Systém</button>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Název role</th>
|
|
||||||
<th>Popis</th>
|
|
||||||
<th>Oprávnění</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 3 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="fw-500">admin</td>
|
|
||||||
<td>Správa celého systému</td>
|
|
||||||
<td>
|
|
||||||
<span className="admin-badge admin-badge-info">
|
|
||||||
všechna
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn-icon" title="Upravit">
|
|
||||||
✎
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-header">
|
|
||||||
<h2 className="admin-card-title">Docházka</h2>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-form">
|
|
||||||
<div className="admin-form-row">
|
|
||||||
<label className="admin-form-label">
|
|
||||||
Limit pro přestávku (hodiny)
|
|
||||||
</label>
|
|
||||||
<input className="admin-form-input" readOnly value="6" />
|
|
||||||
<label className="admin-form-label">
|
|
||||||
Délka krátké přestávky (min)
|
|
||||||
</label>
|
|
||||||
<input className="admin-form-input" readOnly value="15" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
export default function TripsAdminFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Správa jízd</h1>
|
|
||||||
<p className="admin-page-subtitle">10 jízd</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th>Vozidlo</th>
|
|
||||||
<th>Uživatel</th>
|
|
||||||
<th>Km</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="admin-mono">1. 1. 2024</td>
|
|
||||||
<td>Škoda Octavia</td>
|
|
||||||
<td>Jan Novák</td>
|
|
||||||
<td className="admin-mono">200</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn-icon">✎</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
export default function TripsFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Kniha jízd</h1>
|
|
||||||
<p className="admin-page-subtitle">duben 2025</p>
|
|
||||||
</div>
|
|
||||||
<div className="admin-page-actions">
|
|
||||||
<button className="admin-btn admin-btn-primary">Přidat jízdu</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-grid admin-grid-4">
|
|
||||||
<div className="admin-stat-card info">
|
|
||||||
<div className="admin-stat-icon info">
|
|
||||||
<svg
|
|
||||||
width="22"
|
|
||||||
height="22"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<line x1="12" y1="20" x2="12" y2="10" />
|
|
||||||
<line x1="18" y1="20" x2="18" y2="4" />
|
|
||||||
<line x1="6" y1="20" x2="6" y2="16" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="admin-stat-content">
|
|
||||||
<span className="admin-stat-value">12</span>
|
|
||||||
<span className="admin-stat-label">Počet jízd</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-stat-card">
|
|
||||||
<div className="admin-stat-icon">
|
|
||||||
<svg
|
|
||||||
width="22"
|
|
||||||
height="22"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="admin-stat-content">
|
|
||||||
<span className="admin-stat-value">1 240 km</span>
|
|
||||||
<span className="admin-stat-label">Celkem naježděno</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-stat-card success">
|
|
||||||
<div className="admin-stat-icon success">
|
|
||||||
<svg
|
|
||||||
width="22"
|
|
||||||
height="22"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<rect x="1" y="3" width="15" height="13" rx="2" ry="2" />
|
|
||||||
<path d="M16 8h2a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-1" />
|
|
||||||
<circle cx="5.5" cy="18" r="2" />
|
|
||||||
<circle cx="18.5" cy="18" r="2" />
|
|
||||||
<path d="M8 18h8" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="admin-stat-content">
|
|
||||||
<span className="admin-stat-value">980 km</span>
|
|
||||||
<span className="admin-stat-label">Služební</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-stat-card warning">
|
|
||||||
<div className="admin-stat-icon warning">
|
|
||||||
<svg
|
|
||||||
width="22"
|
|
||||||
height="22"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
|
||||||
<polyline points="9 22 9 12 15 12 15 22" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="admin-stat-content">
|
|
||||||
<span className="admin-stat-value">260 km</span>
|
|
||||||
<span className="admin-stat-label">Soukromé</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card mt-6">
|
|
||||||
<div className="admin-card-header flex-between">
|
|
||||||
<h2 className="admin-card-title">Poslední jízdy</h2>
|
|
||||||
<a className="admin-btn admin-btn-secondary admin-btn-sm">
|
|
||||||
Zobrazit historii
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th>Vozidlo</th>
|
|
||||||
<th>Trasa</th>
|
|
||||||
<th>Vzdálenost</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 4 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="admin-mono">1. 4. 2025</td>
|
|
||||||
<td>
|
|
||||||
<span className="admin-badge">1A2 3456</span>
|
|
||||||
</td>
|
|
||||||
<td>Praha → Brno</td>
|
|
||||||
<td className="admin-mono">
|
|
||||||
<strong>200 km</strong>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
export default function TripsHistoryFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Historie jízd</h1>
|
|
||||||
<p className="admin-page-subtitle">10 jízd</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th>Vozidlo</th>
|
|
||||||
<th>Uživatel</th>
|
|
||||||
<th>Trasa</th>
|
|
||||||
<th>Km</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="admin-mono">1. 1. 2024</td>
|
|
||||||
<td>Škoda Octavia</td>
|
|
||||||
<td>Jan Novák</td>
|
|
||||||
<td>Praha → Brno</td>
|
|
||||||
<td className="admin-mono">200</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
export default function UsersFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Uživatelé</h1>
|
|
||||||
<p className="admin-page-subtitle">5 uživatelů</p>
|
|
||||||
</div>
|
|
||||||
<button className="admin-btn admin-btn-primary">
|
|
||||||
+ Přidat uživatele
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-search-bar mb-4">
|
|
||||||
<input className="admin-form-input" placeholder="" />
|
|
||||||
</div>
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Uživatel</th>
|
|
||||||
<th>E-mail</th>
|
|
||||||
<th>Role</th>
|
|
||||||
<th>Stav</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-user">
|
|
||||||
<div className="admin-table-avatar">A</div>
|
|
||||||
<div>
|
|
||||||
<div className="admin-table-name">Jan Novák</div>
|
|
||||||
<div className="admin-table-username">@jan</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>jan@email.cz</td>
|
|
||||||
<td>
|
|
||||||
<span className="admin-badge admin-badge-admin">
|
|
||||||
Administrátor
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span className="admin-badge admin-badge-active">
|
|
||||||
Aktivní
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn-icon">✎</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
export default function VehiclesFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<h1 className="admin-page-title">Vozidla</h1>
|
|
||||||
<button className="admin-btn admin-btn-primary">
|
|
||||||
+ Přidat vozidlo
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-search-bar mb-4">
|
|
||||||
<input className="admin-form-input" placeholder="" />
|
|
||||||
</div>
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Značka</th>
|
|
||||||
<th>Model</th>
|
|
||||||
<th>SPZ</th>
|
|
||||||
<th>Rok</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>Škoda</td>
|
|
||||||
<td>Octavia</td>
|
|
||||||
<td className="admin-mono">1A2 3456</td>
|
|
||||||
<td>2024</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn-icon">✎</button>
|
|
||||||
<button className="admin-btn-icon danger">🗑</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
|
import { isHoliday } from "../../utils/czech-holidays";
|
||||||
import {
|
import {
|
||||||
calcProjectMinutesTotal,
|
calcProjectMinutesTotal,
|
||||||
calcFormWorkMinutes,
|
calcFormWorkMinutes,
|
||||||
@@ -10,9 +11,17 @@ import {
|
|||||||
formatDate,
|
formatDate,
|
||||||
formatMinutes,
|
formatMinutes,
|
||||||
getLeaveTypeName,
|
getLeaveTypeName,
|
||||||
getLeaveTypeBadgeClass,
|
|
||||||
formatTimeOrDatetimePrint,
|
formatTimeOrDatetimePrint,
|
||||||
calculateWorkMinutesPrint,
|
calculateWorkMinutesPrint,
|
||||||
|
isWeekendDate,
|
||||||
|
getDaysInMonth,
|
||||||
|
shiftDateForMonth,
|
||||||
|
formatHoursDecimal,
|
||||||
|
calculateNightMinutes,
|
||||||
|
normalizeDateStr,
|
||||||
|
holidaysInMonth,
|
||||||
|
calculateFreeHolidayHours,
|
||||||
|
getCzechWeekday,
|
||||||
} from "../utils/attendanceHelpers";
|
} from "../utils/attendanceHelpers";
|
||||||
import type {
|
import type {
|
||||||
ShiftFormData,
|
ShiftFormData,
|
||||||
@@ -29,7 +38,7 @@ interface AlertContext {
|
|||||||
alert: { success: (msg: string) => void; error: (msg: string) => void };
|
alert: { success: (msg: string) => void; error: (msg: string) => void };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AttendanceRecord {
|
export interface AttendanceRecord {
|
||||||
id: number;
|
id: number;
|
||||||
user_id: number;
|
user_id: number;
|
||||||
shift_date: string;
|
shift_date: string;
|
||||||
@@ -39,7 +48,7 @@ interface AttendanceRecord {
|
|||||||
departure_time?: string | null;
|
departure_time?: string | null;
|
||||||
break_start?: string | null;
|
break_start?: string | null;
|
||||||
break_end?: string | null;
|
break_end?: string | null;
|
||||||
notes?: string;
|
notes?: string | null;
|
||||||
project_id?: number | null;
|
project_id?: number | null;
|
||||||
project_name?: string;
|
project_name?: string;
|
||||||
project_logs?: Array<{
|
project_logs?: Array<{
|
||||||
@@ -73,7 +82,6 @@ interface UserTotal {
|
|||||||
working: boolean;
|
working: boolean;
|
||||||
vacation_hours: number;
|
vacation_hours: number;
|
||||||
sick_hours: number;
|
sick_hours: number;
|
||||||
holiday_hours: number;
|
|
||||||
unpaid_hours: number;
|
unpaid_hours: number;
|
||||||
overtime: number;
|
overtime: number;
|
||||||
missing: number;
|
missing: number;
|
||||||
@@ -81,6 +89,16 @@ interface UserTotal {
|
|||||||
business_days: number;
|
business_days: number;
|
||||||
worked_hours: number;
|
worked_hours: number;
|
||||||
covered: number;
|
covered: number;
|
||||||
|
// mzda-style summary fields (set by computeUserTotals)
|
||||||
|
worked_hours_raw?: number;
|
||||||
|
odpracovano?: number;
|
||||||
|
vcetne_svatku?: number;
|
||||||
|
prescas?: number;
|
||||||
|
svatek?: number;
|
||||||
|
weekend_hours?: number;
|
||||||
|
night_minutes?: number;
|
||||||
|
worked_holiday_hours?: number; // sum of hours worked ON a holiday
|
||||||
|
records?: AttendanceRecord[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LeaveBalance {
|
interface LeaveBalance {
|
||||||
@@ -121,6 +139,11 @@ const combineDatetime = (date: string, time: string): string | null =>
|
|||||||
/**
|
/**
|
||||||
* Compute per-user totals from raw attendance records.
|
* Compute per-user totals from raw attendance records.
|
||||||
* This replaces the server-side `user_totals` that the PHP backend returned.
|
* This replaces the server-side `user_totals` that the PHP backend returned.
|
||||||
|
*
|
||||||
|
* Adds mzda-style summary fields (odpracovano, vcetne_svatku, prescas, svatek,
|
||||||
|
* weekend_hours, night_minutes) and a fund computed as
|
||||||
|
* (rawBizDays + holidayCount) × 8 — holidays count as work days for fund
|
||||||
|
* purposes, per the mzda.pdf model.
|
||||||
*/
|
*/
|
||||||
function computeUserTotals(
|
function computeUserTotals(
|
||||||
records: AttendanceRecord[],
|
records: AttendanceRecord[],
|
||||||
@@ -128,6 +151,7 @@ function computeUserTotals(
|
|||||||
month: string,
|
month: string,
|
||||||
): Record<string, UserTotal> {
|
): Record<string, UserTotal> {
|
||||||
const totals: Record<string, UserTotal> = {};
|
const totals: Record<string, UserTotal> = {};
|
||||||
|
const recordsByUser = new Map<number, AttendanceRecord[]>();
|
||||||
|
|
||||||
for (const rec of records) {
|
for (const rec of records) {
|
||||||
const uid = String(rec.user_id);
|
const uid = String(rec.user_id);
|
||||||
@@ -144,7 +168,6 @@ function computeUserTotals(
|
|||||||
working: false,
|
working: false,
|
||||||
vacation_hours: 0,
|
vacation_hours: 0,
|
||||||
sick_hours: 0,
|
sick_hours: 0,
|
||||||
holiday_hours: 0,
|
|
||||||
unpaid_hours: 0,
|
unpaid_hours: 0,
|
||||||
overtime: 0,
|
overtime: 0,
|
||||||
missing: 0,
|
missing: 0,
|
||||||
@@ -152,15 +175,23 @@ function computeUserTotals(
|
|||||||
business_days: 0,
|
business_days: 0,
|
||||||
worked_hours: 0,
|
worked_hours: 0,
|
||||||
covered: 0,
|
covered: 0,
|
||||||
|
records: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = totals[uid];
|
const t = totals[uid];
|
||||||
|
t.records!.push(rec);
|
||||||
|
if (!recordsByUser.has(rec.user_id)) recordsByUser.set(rec.user_id, []);
|
||||||
|
recordsByUser.get(rec.user_id)!.push(rec);
|
||||||
|
|
||||||
const leaveType = rec.leave_type || "work";
|
const leaveType = rec.leave_type || "work";
|
||||||
|
|
||||||
if (leaveType === "work") {
|
if (leaveType === "work") {
|
||||||
// Only work records contribute to "minutes" (matching PHP calculateUserTotals)
|
// Only work records contribute to "minutes" (matching PHP calculateUserTotals)
|
||||||
t.minutes += calculateWorkMinutes(rec);
|
t.minutes += calculateWorkMinutes({
|
||||||
|
...rec,
|
||||||
|
notes: rec.notes ?? undefined,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const leaveHours = Number(rec.leave_hours) || 8;
|
const leaveHours = Number(rec.leave_hours) || 8;
|
||||||
switch (leaveType) {
|
switch (leaveType) {
|
||||||
@@ -170,12 +201,14 @@ function computeUserTotals(
|
|||||||
case "sick":
|
case "sick":
|
||||||
t.sick_hours += leaveHours;
|
t.sick_hours += leaveHours;
|
||||||
break;
|
break;
|
||||||
case "holiday":
|
|
||||||
t.holiday_hours += leaveHours;
|
|
||||||
break;
|
|
||||||
case "unpaid":
|
case "unpaid":
|
||||||
t.unpaid_hours += leaveHours;
|
t.unpaid_hours += leaveHours;
|
||||||
break;
|
break;
|
||||||
|
// "holiday" leave_type is no longer selectable in the UI — it is
|
||||||
|
// auto-computed from Czech public holidays (see freeHolidayHours
|
||||||
|
// below). Old records may still exist in the DB; treat them as a
|
||||||
|
// paid non-work entry and skip so they don't double-count with
|
||||||
|
// the auto-detected svátek.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +223,10 @@ function computeUserTotals(
|
|||||||
const yr = parseInt(yearStr, 10);
|
const yr = parseInt(yearStr, 10);
|
||||||
const mo = parseInt(monthStr, 10) - 1;
|
const mo = parseInt(monthStr, 10) - 1;
|
||||||
|
|
||||||
// Count business days in month (Mon-Fri)
|
// Count Mon-Fri business days in month (INCLUDING holidays).
|
||||||
|
// The fund is rawBizDays × 8 — holidays stay in the count so the
|
||||||
|
// user is paid for them via the fund. Svátek (free holiday hours) is
|
||||||
|
// computed separately as "8h per holiday the user did not work".
|
||||||
let rawBizDays = 0;
|
let rawBizDays = 0;
|
||||||
const cur = new Date(yr, mo, 1);
|
const cur = new Date(yr, mo, 1);
|
||||||
while (cur.getMonth() === mo) {
|
while (cur.getMonth() === mo) {
|
||||||
@@ -199,23 +235,100 @@ function computeUserTotals(
|
|||||||
cur.setDate(cur.getDate() + 1);
|
cur.setDate(cur.getDate() + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const holidayDates = holidaysInMonth(yr, mo + 1);
|
||||||
|
const holidayCount = holidayDates.length;
|
||||||
|
|
||||||
for (const uid of Object.keys(totals)) {
|
for (const uid of Object.keys(totals)) {
|
||||||
const t = totals[uid];
|
const t = totals[uid];
|
||||||
// Subtract holiday days from business days for this user
|
const userRecords = recordsByUser.get(Number(uid)) || [];
|
||||||
const holidayDays = Math.round(t.holiday_hours / 8);
|
|
||||||
const bizDays = Math.max(0, rawBizDays - holidayDays);
|
// Odpracováno: floor(worked / 8) × 8 — round to whole days, drop remainder.
|
||||||
const fund = bizDays * 8;
|
const workedHoursRaw = t.minutes / 60;
|
||||||
const workedHours = Math.round((t.minutes / 60) * 10) / 10;
|
const odpracovano = Math.floor(workedHoursRaw / 8) * 8;
|
||||||
// Covered = worked + vacation + sick (NOT holiday/unpaid — matching PHP)
|
|
||||||
const leaveHours = t.vacation_hours + t.sick_hours;
|
// So/Ne: sum of worked hours on Sat/Sun (raw, not rounded).
|
||||||
const covered = Math.round((workedHours + leaveHours) * 10) / 10;
|
const weekendHours = userRecords
|
||||||
|
.filter(
|
||||||
|
(r) =>
|
||||||
|
(r.leave_type || "work") === "work" && isWeekendDate(r.shift_date),
|
||||||
|
)
|
||||||
|
.reduce((sum, r) => sum + calculateWorkMinutesPrint(r) / 60, 0);
|
||||||
|
|
||||||
|
// Práce v noci: minutes in 22:00-06:00 across all work records.
|
||||||
|
const nightMinutes = userRecords
|
||||||
|
.filter((r) => (r.leave_type || "work") === "work")
|
||||||
|
.reduce((sum, r) => sum + calculateNightMinutes(r), 0);
|
||||||
|
|
||||||
|
// Svátek (free): 8h per holiday the user did not work.
|
||||||
|
const freeHolidayHours = calculateFreeHolidayHours(
|
||||||
|
userRecords,
|
||||||
|
holidayDates,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Worked holidays: holidays the user DID work (counted toward
|
||||||
|
// Odpracováno, but their 8h must not flow into Přesčas).
|
||||||
|
const workedDatesSet = new Set(
|
||||||
|
userRecords
|
||||||
|
.filter((r) => (r.leave_type || "work") === "work")
|
||||||
|
.map((r) => normalizeDateStr(r.shift_date))
|
||||||
|
.filter(Boolean),
|
||||||
|
);
|
||||||
|
let workedHolidayCount = 0;
|
||||||
|
let workedHolidayHours = 0;
|
||||||
|
for (const hd of holidayDates) {
|
||||||
|
if (workedDatesSet.has(hd)) workedHolidayCount++;
|
||||||
|
}
|
||||||
|
// Sum the actual hours worked on holiday dates (for the KPI card's
|
||||||
|
// "fond used" total = real_work + vacation + worked_holiday + free_holiday).
|
||||||
|
for (const r of userRecords) {
|
||||||
|
if ((r.leave_type || "work") !== "work") continue;
|
||||||
|
const d = normalizeDateStr(r.shift_date);
|
||||||
|
if (d && holidayDates.includes(d)) {
|
||||||
|
workedHolidayHours += calculateWorkMinutesPrint(r) / 60;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fund = Mon-Fri × 8h. Holidays count as work days (already in rawBizDays).
|
||||||
|
const fund = rawBizDays * 8;
|
||||||
|
|
||||||
|
// Včetně svátků a přesčasů (mzda formula):
|
||||||
|
// odpracovano + vacation + remainder − min(freeHols, workedHols × 8)
|
||||||
|
//
|
||||||
|
// Two-way logic matching the print:
|
||||||
|
// • If the user worked at least one holiday, that worked holiday's
|
||||||
|
// 8h is in Odpracováno and shouldn't also count as Přesčas. We
|
||||||
|
// subtract one 8h per worked holiday (capped at the total free
|
||||||
|
// holiday hours — can't go negative).
|
||||||
|
// • If the user did NOT work any holiday, no adjustment is needed:
|
||||||
|
// all unworked holidays are already paid via the Svátek line and
|
||||||
|
// don't flow into Přesčas, so vcetne_svatku just equals the
|
||||||
|
// work + vacation + remainder.
|
||||||
|
const remainder = workedHoursRaw - odpracovano;
|
||||||
|
const holidayAdj = Math.min(freeHolidayHours, workedHolidayCount * 8);
|
||||||
|
const vcetneSv = odpracovano - holidayAdj + t.vacation_hours + remainder;
|
||||||
|
|
||||||
|
// Přesčas = vcetneSv − odpracovano (derived).
|
||||||
|
const prescas = vcetneSv - odpracovano;
|
||||||
|
|
||||||
|
// Legacy fields (kept so existing UI doesn't break).
|
||||||
|
const workedHours = Math.round(workedHoursRaw * 10) / 10;
|
||||||
|
const covered = Math.round((workedHours + t.vacation_hours) * 10) / 10;
|
||||||
|
|
||||||
t.fund = fund;
|
t.fund = fund;
|
||||||
t.business_days = bizDays;
|
t.business_days = rawBizDays;
|
||||||
t.worked_hours = workedHours;
|
t.worked_hours = workedHours;
|
||||||
t.covered = covered;
|
t.covered = covered;
|
||||||
t.missing = Math.max(0, Math.round((fund - covered) * 10) / 10);
|
t.missing = Math.max(0, Math.round((fund - covered) * 10) / 10);
|
||||||
t.overtime = Math.max(0, Math.round((covered - fund) * 10) / 10);
|
t.overtime = Math.max(0, Math.round((covered - fund) * 10) / 10);
|
||||||
|
|
||||||
|
t.worked_hours_raw = workedHoursRaw;
|
||||||
|
t.odpracovano = odpracovano;
|
||||||
|
t.vcetne_svatku = vcetneSv;
|
||||||
|
t.prescas = prescas;
|
||||||
|
t.svatek = freeHolidayHours;
|
||||||
|
t.weekend_hours = weekendHours;
|
||||||
|
t.night_minutes = nightMinutes;
|
||||||
|
t.worked_holiday_hours = workedHolidayHours;
|
||||||
}
|
}
|
||||||
|
|
||||||
return totals;
|
return totals;
|
||||||
@@ -234,14 +347,6 @@ function escapeHtml(str: string): string {
|
|||||||
.replace(/'/g, "'");
|
.replace(/'/g, "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFundStatus(userData: Record<string, any>): string {
|
|
||||||
if (userData.overtime > 0)
|
|
||||||
return `<span class="leave-badge badge-overtime">+${escapeHtml(String(userData.overtime))}h přesčas</span>`;
|
|
||||||
if (userData.missing > 0)
|
|
||||||
return `<span style="color:#dc2626">−${escapeHtml(String(userData.missing))}h</span>`;
|
|
||||||
return '<span style="color:#16a34a">splněno</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildProjectLogsHtml(record: Record<string, any>): string {
|
function buildProjectLogsHtml(record: Record<string, any>): string {
|
||||||
if (record.project_logs && record.project_logs.length > 0) {
|
if (record.project_logs && record.project_logs.length > 0) {
|
||||||
return record.project_logs
|
return record.project_logs
|
||||||
@@ -272,36 +377,55 @@ function buildProjectLogsHtml(record: Record<string, any>): string {
|
|||||||
return escapeHtml(record.project_name || "—");
|
return escapeHtml(record.project_name || "—");
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildLeaveSummaryHtml(
|
|
||||||
userId: string,
|
|
||||||
userData: Record<string, any>,
|
|
||||||
printData: Record<string, any>,
|
|
||||||
): string {
|
|
||||||
const bal = printData.leave_balances[userId];
|
|
||||||
let parts = `<strong>Dovolená ${escapeHtml(String(printData.year))}:</strong> Zbývá ${bal.vacation_remaining.toFixed(1)}h z ${bal.vacation_total}h`;
|
|
||||||
if (userData.vacation_hours > 0)
|
|
||||||
parts += ` | <span class="leave-badge badge-vacation">Tento měsíc: ${escapeHtml(String(userData.vacation_hours))}h</span>`;
|
|
||||||
if (userData.sick_hours > 0)
|
|
||||||
parts += ` | <span class="leave-badge badge-sick">Nemoc: ${escapeHtml(String(userData.sick_hours))}h</span>`;
|
|
||||||
if (userData.holiday_hours > 0)
|
|
||||||
parts += ` | <span class="leave-badge badge-holiday">Svátek: ${escapeHtml(String(userData.holiday_hours))}h</span>`;
|
|
||||||
if (userData.overtime > 0)
|
|
||||||
parts += ` | <span class="leave-badge badge-overtime">Přesčas: +${escapeHtml(String(userData.overtime))}h</span>`;
|
|
||||||
return `<div class="leave-summary">${parts}</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildUserSectionHtml(
|
function buildUserSectionHtml(
|
||||||
userId: string,
|
userId: string,
|
||||||
userData: Record<string, any>,
|
userData: Record<string, any>,
|
||||||
printData: Record<string, any>,
|
printData: Record<string, any>,
|
||||||
): string {
|
): string {
|
||||||
const leaveHtml = printData.leave_balances[userId]
|
// Build a date-keyed lookup of the user's records.
|
||||||
? buildLeaveSummaryHtml(userId, userData, printData)
|
const recordsByDate = new Map<string, Record<string, any>>();
|
||||||
: "";
|
for (const r of userData.records || []) {
|
||||||
|
recordsByDate.set(normalizeDateStr(r.shift_date), r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate one row per day of the month.
|
||||||
|
const [yearStr, monthStr] = printData.month.split("-");
|
||||||
|
const yr = parseInt(yearStr, 10);
|
||||||
|
const mo = parseInt(monthStr, 10);
|
||||||
|
const daysInMonth = getDaysInMonth(yr, mo);
|
||||||
|
|
||||||
|
const userRecords = (userData.records || []) as Record<string, any>[];
|
||||||
|
|
||||||
|
const rows: string[] = [];
|
||||||
|
for (let d = 1; d <= daysInMonth; d++) {
|
||||||
|
const dateStr = shiftDateForMonth(yr, mo, d);
|
||||||
|
const record = recordsByDate.get(dateStr);
|
||||||
|
const weekend = isWeekendDate(dateStr);
|
||||||
|
const holiday = isHoliday(dateStr);
|
||||||
|
const leaveType = (record?.leave_type as string) || "work";
|
||||||
|
const rowClasses = [
|
||||||
|
weekend && "weekend",
|
||||||
|
holiday && "holiday",
|
||||||
|
leaveType === "vacation" && "vacation",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
rows.push(`<tr class="${rowClasses}">
|
||||||
|
<td>${escapeHtml(formatDate(dateStr))}</td>
|
||||||
|
<td>${escapeHtml(getCzechWeekday(dateStr))}</td>
|
||||||
|
<td>—</td>
|
||||||
|
<td class="text-center">—</td>
|
||||||
|
<td class="text-center">—</td>
|
||||||
|
<td class="text-center">—</td>
|
||||||
|
<td class="text-center">—</td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const recordRows = (userData.records || [])
|
|
||||||
.map((record: any) => {
|
|
||||||
const leaveType = record.leave_type || "work";
|
|
||||||
const isLeave = leaveType !== "work";
|
const isLeave = leaveType !== "work";
|
||||||
const workMinutes = calculateWorkMinutesPrint(record);
|
const workMinutes = calculateWorkMinutesPrint(record);
|
||||||
const hours = Math.floor(workMinutes / 60);
|
const hours = Math.floor(workMinutes / 60);
|
||||||
@@ -311,55 +435,162 @@ function buildUserSectionHtml(
|
|||||||
? "—"
|
? "—"
|
||||||
: `${escapeHtml(formatTimeOrDatetimePrint(record.break_start, record.shift_date))} - ${escapeHtml(formatTimeOrDatetimePrint(record.break_end, record.shift_date))}`;
|
: `${escapeHtml(formatTimeOrDatetimePrint(record.break_start, record.shift_date))} - ${escapeHtml(formatTimeOrDatetimePrint(record.break_end, record.shift_date))}`;
|
||||||
|
|
||||||
return `<tr>
|
let hoursCell: string;
|
||||||
<td>${escapeHtml(formatDate(record.shift_date))}</td>
|
if (workMinutes > 0 && !isLeave) {
|
||||||
<td><span class="leave-badge ${escapeHtml(getLeaveTypeBadgeClass(leaveType))}">${escapeHtml(getLeaveTypeName(leaveType))}</span></td>
|
hoursCell = `${hours}:${String(mins).padStart(2, "0")} (${formatHoursDecimal(workMinutes)})`;
|
||||||
|
} else if (isLeave) {
|
||||||
|
hoursCell = formatHoursDecimal((Number(record.leave_hours) || 8) * 60);
|
||||||
|
} else {
|
||||||
|
hoursCell = "—";
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.push(`<tr class="${rowClasses}">
|
||||||
|
<td>${escapeHtml(formatDate(dateStr))}</td>
|
||||||
|
<td>${escapeHtml(getCzechWeekday(dateStr))}</td>
|
||||||
|
<td>${escapeHtml(getLeaveTypeName(leaveType))}</td>
|
||||||
<td class="text-center">${isLeave ? "—" : escapeHtml(formatTimeOrDatetimePrint(record.arrival_time, record.shift_date))}</td>
|
<td class="text-center">${isLeave ? "—" : escapeHtml(formatTimeOrDatetimePrint(record.arrival_time, record.shift_date))}</td>
|
||||||
<td class="text-center">${breakCell}</td>
|
<td class="text-center">${breakCell}</td>
|
||||||
<td class="text-center">${isLeave ? "—" : escapeHtml(formatTimeOrDatetimePrint(record.departure_time, record.shift_date))}</td>
|
<td class="text-center">${isLeave ? "—" : escapeHtml(formatTimeOrDatetimePrint(record.departure_time, record.shift_date))}</td>
|
||||||
<td class="text-center">${workMinutes > 0 ? `${hours}:${String(mins).padStart(2, "0")}` : "—"}</td>
|
<td class="text-center">${hoursCell}</td>
|
||||||
<td style="font-size:8px">${buildProjectLogsHtml(record)}</td>
|
<td style="font-size:8px">${buildProjectLogsHtml(record)}</td>
|
||||||
<td>${escapeHtml(record.notes || "")}</td>
|
<td>${escapeHtml(record.notes || "")}</td>
|
||||||
</tr>`;
|
</tr>`);
|
||||||
})
|
}
|
||||||
.join("");
|
|
||||||
|
|
||||||
const fundRow =
|
// ----- mzda-style summary numbers (computed here, not from userData,
|
||||||
userData.fund !== null
|
// because the backend's getPrintData only returns the legacy fields). -----
|
||||||
? `<tr>
|
|
||||||
<td colspan="6" class="text-right">Fond měsíce:</td>
|
// Worked minutes: sum of calculateWorkMinutesPrint across work records.
|
||||||
<td class="text-center">${escapeHtml(String(userData.covered))}h / ${escapeHtml(String(userData.fund))}h</td>
|
let workedMinutes = 0;
|
||||||
<td colspan="2">${renderFundStatus(userData)}</td>
|
let weekendHoursRaw = 0;
|
||||||
</tr>`
|
let nightMinutes = 0;
|
||||||
: "";
|
for (const r of userRecords) {
|
||||||
|
const leaveType = r.leave_type || "work";
|
||||||
|
if (leaveType !== "work") continue;
|
||||||
|
const m = calculateWorkMinutesPrint(r);
|
||||||
|
workedMinutes += m;
|
||||||
|
if (isWeekendDate(r.shift_date)) {
|
||||||
|
weekendHoursRaw += m / 60;
|
||||||
|
}
|
||||||
|
nightMinutes += calculateNightMinutes(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum of vacation/sick/unpaid hours (in hours, not minutes). The
|
||||||
|
// "holiday" leave_type is auto-computed from Czech holidays further down
|
||||||
|
// and no longer selectable in the UI, so it's deliberately omitted here.
|
||||||
|
let vacationHours = 0,
|
||||||
|
sickHours = 0;
|
||||||
|
for (const r of userRecords) {
|
||||||
|
const lt = r.leave_type || "work";
|
||||||
|
if (lt === "work") continue;
|
||||||
|
const h = Number(r.leave_hours) || 8;
|
||||||
|
if (lt === "vacation") vacationHours += h;
|
||||||
|
else if (lt === "sick") sickHours += h;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Odpracováno: floor(worked / 8) × 8.
|
||||||
|
const workedHoursRaw = workedMinutes / 60;
|
||||||
|
const odpracovano = Math.floor(workedHoursRaw / 8) * 8;
|
||||||
|
|
||||||
|
// Free Svátek: 8h per holiday date the user did not work.
|
||||||
|
const holidayDates = holidaysInMonth(yr, mo);
|
||||||
|
const holidayCount = holidayDates.length;
|
||||||
|
const workedDates = new Set(
|
||||||
|
userRecords
|
||||||
|
.filter((r) => (r.leave_type || "work") === "work")
|
||||||
|
.map((r) => normalizeDateStr(r.shift_date))
|
||||||
|
.filter(Boolean),
|
||||||
|
);
|
||||||
|
let freeHolidayHours = 0;
|
||||||
|
let workedHolidayCount = 0;
|
||||||
|
for (const hd of holidayDates) {
|
||||||
|
if (workedDates.has(hd)) workedHolidayCount++;
|
||||||
|
else freeHolidayHours += 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fund: count Mon-Fri (incl. holidays) × 8h.
|
||||||
|
let rawBizDays = 0;
|
||||||
|
const cur = new Date(yr, mo - 1, 1);
|
||||||
|
while (cur.getMonth() === mo - 1) {
|
||||||
|
const dow = cur.getDay();
|
||||||
|
if (dow !== 0 && dow !== 6) rawBizDays++;
|
||||||
|
cur.setDate(cur.getDate() + 1);
|
||||||
|
}
|
||||||
|
const businessDays = rawBizDays;
|
||||||
|
const fund = businessDays * 8;
|
||||||
|
|
||||||
|
// Včetně svátků a přesčasů: floor(worked/8)*8 − worked_holiday*8 + vacation + remainder.
|
||||||
|
// (A worked holiday counts toward Odpracováno, but its 8h should not flow
|
||||||
|
// into Přesčas — so we subtract it here. Unworked holidays are paid via
|
||||||
|
// the fund and don't affect this line.)
|
||||||
|
const remainder = workedHoursRaw - odpracovano;
|
||||||
|
const vcetneSv =
|
||||||
|
odpracovano - workedHolidayCount * 8 + vacationHours + remainder;
|
||||||
|
|
||||||
|
// Přesčas = #2 - #1.
|
||||||
|
const prescas = vcetneSv - odpracovano;
|
||||||
|
|
||||||
|
const summaryHtml = `<div class="user-summary">
|
||||||
|
<div class="summary-row">
|
||||||
|
<span class="summary-label">Odpracováno:</span>
|
||||||
|
<span class="summary-value">${formatHoursDecimal(odpracovano * 60)}h</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span class="summary-label">Odpracováno včetně svátků a přesčasů:</span>
|
||||||
|
<span class="summary-value">${formatHoursDecimal(vcetneSv * 60)}h</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span class="summary-label">Dovolená:</span>
|
||||||
|
<span class="summary-value">${formatHoursDecimal(vacationHours * 60)}h</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span class="summary-label">Přesčas:</span>
|
||||||
|
<span class="summary-value">${formatHoursDecimal(prescas * 60)}h</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span class="summary-label">Svátek:</span>
|
||||||
|
<span class="summary-value">${formatHoursDecimal(freeHolidayHours * 60)}h</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span class="summary-label">So/Ne:</span>
|
||||||
|
<span class="summary-value">${formatHoursDecimal(weekendHoursRaw * 60)}h</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span class="summary-label">Práce v noci:</span>
|
||||||
|
<span class="summary-value">${formatHoursDecimal(nightMinutes)}h</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row summary-fund">
|
||||||
|
<span class="summary-label">Fond měsíce:</span>
|
||||||
|
<span class="summary-value">${formatHoursDecimal(fund * 60)}h (${businessDays} dnů)</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend">
|
||||||
|
<span class="legend-item"><span class="legend-swatch weekend"></span>Víkend (So/Ne)</span>
|
||||||
|
<span class="legend-item"><span class="legend-swatch holiday"></span>Svátek</span>
|
||||||
|
<span class="legend-item"><span class="legend-swatch weekend-holiday"></span>Víkend + svátek</span>
|
||||||
|
<span class="legend-item"><span class="legend-swatch vacation"></span>Dovolená</span>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
return `<div class="user-section">
|
return `<div class="user-section">
|
||||||
<div class="user-header">
|
<div class="user-header">
|
||||||
<h3>${escapeHtml(userData.name)}</h3>
|
<h3>${escapeHtml(userData.name)}</h3>
|
||||||
<span class="total">Odpracováno: ${escapeHtml(formatMinutes(userData.minutes))} h</span>
|
<span class="total">Odpracováno: ${escapeHtml(formatMinutes(userData.minutes))} h</span>
|
||||||
</div>
|
</div>
|
||||||
${leaveHtml}
|
|
||||||
<table>
|
<table>
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
<th style="width:70px">Datum</th>
|
<th style="width:55px">Datum</th>
|
||||||
<th style="width:70px">Typ</th>
|
<th style="width:55px">Den</th>
|
||||||
<th class="text-center" style="width:70px">Příchod</th>
|
<th style="width:60px">Typ</th>
|
||||||
<th class="text-center" style="width:90px">Pauza</th>
|
<th class="text-center" style="width:55px">Příchod</th>
|
||||||
<th class="text-center" style="width:70px">Odchod</th>
|
<th class="text-center" style="width:80px">Pauza</th>
|
||||||
<th class="text-center" style="width:80px">Hodiny</th>
|
<th class="text-center" style="width:55px">Odchod</th>
|
||||||
|
<th class="text-center" style="width:85px">Hodiny</th>
|
||||||
<th>Projekty</th>
|
<th>Projekty</th>
|
||||||
<th>Poznámka</th>
|
<th>Poznámka</th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>${recordRows}</tbody>
|
<tbody>${rows.join("")}</tbody>
|
||||||
<tfoot>
|
|
||||||
<tr>
|
|
||||||
<td colspan="6" class="text-right">Odpracováno:</td>
|
|
||||||
<td class="text-center">${escapeHtml(formatMinutes(userData.minutes))} h</td>
|
|
||||||
<td colspan="2"></td>
|
|
||||||
</tr>
|
|
||||||
${fundRow}
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
</table>
|
||||||
|
${summaryHtml}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,7 +608,13 @@ function buildPrintHtml(
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Docházka - ${escapeHtml(pData.month_name)}</title>
|
<title>Docházka - ${escapeHtml(pData.month_name)}</title>
|
||||||
<style>
|
<style>
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
font-size: 11px; line-height: 1.4; color: #000; background: #fff; padding: 15mm;
|
font-size: 11px; line-height: 1.4; color: #000; background: #fff; padding: 15mm;
|
||||||
@@ -407,19 +644,47 @@ function buildPrintHtml(
|
|||||||
.user-section th { background: #333; color: #fff; font-weight: 600; font-size: 10px; text-transform: uppercase; }
|
.user-section th { background: #333; color: #fff; font-weight: 600; font-size: 10px; text-transform: uppercase; }
|
||||||
.user-section td { font-size: 10px; }
|
.user-section td { font-size: 10px; }
|
||||||
.user-section tr:nth-child(even) { background: #f9f9f9; }
|
.user-section tr:nth-child(even) { background: #f9f9f9; }
|
||||||
|
/* Weekend and holiday row highlighting — must come after the
|
||||||
|
:nth-child(even) rule and use higher specificity (tr.X td) to win. */
|
||||||
|
.user-section tr.weekend td { background: #fde68a; }
|
||||||
|
.user-section tr.holiday td { background: #bbf7d0; }
|
||||||
|
.user-section tr.weekend.holiday td { background: #fcd34d; }
|
||||||
|
.user-section tr.vacation td { background: #bfdbfe; }
|
||||||
.text-center { text-align: center; }
|
.text-center { text-align: center; }
|
||||||
.text-right { text-align: right; }
|
.text-right { text-align: right; }
|
||||||
.user-section tfoot td { background: #eee; font-weight: 600; }
|
|
||||||
.leave-badge { display: inline-block; padding: 2px 6px; border-radius: 3px; font-size: 9px; font-weight: 500; }
|
.leave-badge { display: inline-block; padding: 2px 6px; border-radius: 3px; font-size: 9px; font-weight: 500; }
|
||||||
.badge-vacation { background: #dbeafe; color: #1d4ed8; }
|
.badge-vacation { background: #dbeafe; color: #1d4ed8; }
|
||||||
.badge-sick { background: #fee2e2; color: #dc2626; }
|
.badge-sick { background: #fee2e2; color: #dc2626; }
|
||||||
.badge-holiday { background: #dcfce7; color: #16a34a; }
|
.badge-holiday { background: #dcfce7; color: #16a34a; }
|
||||||
.badge-unpaid { background: #f3f4f6; color: #6b7280; }
|
.badge-unpaid { background: #f3f4f6; color: #6b7280; }
|
||||||
.badge-overtime { background: #fef3c7; color: #d97706; }
|
.badge-overtime { background: #fef3c7; color: #d97706; }
|
||||||
.leave-summary {
|
.user-summary {
|
||||||
margin-top: 10px; padding: 8px 15px; background: #f9f9f9;
|
margin-top: 10px; padding: 10px 15px;
|
||||||
border: 1px solid #ddd; font-size: 10px;
|
background: #f9f9f9; border: 1px solid #ddd; font-size: 10px;
|
||||||
}
|
}
|
||||||
|
.user-summary .summary-row {
|
||||||
|
display: flex; justify-content: space-between; padding: 2px 0;
|
||||||
|
}
|
||||||
|
.user-summary .summary-label { color: #555; }
|
||||||
|
.user-summary .summary-value { font-weight: 600; }
|
||||||
|
.user-summary .summary-fund {
|
||||||
|
border-top: 1px solid #ddd; margin-top: 4px; padding-top: 6px;
|
||||||
|
}
|
||||||
|
.legend {
|
||||||
|
display: flex; flex-wrap: wrap; gap: 14px;
|
||||||
|
margin-top: 8px; padding-top: 6px;
|
||||||
|
border-top: 1px dashed #ddd;
|
||||||
|
font-size: 9px; color: #555;
|
||||||
|
}
|
||||||
|
.legend-item { display: inline-flex; align-items: center; gap: 5px; }
|
||||||
|
.legend-swatch {
|
||||||
|
display: inline-block; width: 12px; height: 12px;
|
||||||
|
border: 1px solid #333; vertical-align: middle;
|
||||||
|
}
|
||||||
|
.legend-swatch.weekend { background: #fde68a; }
|
||||||
|
.legend-swatch.holiday { background: #bbf7d0; }
|
||||||
|
.legend-swatch.weekend-holiday { background: #fcd34d; }
|
||||||
|
.legend-swatch.vacation { background: #bfdbfe; }
|
||||||
.print-wrapper-table { width: 100%; border-collapse: collapse; border: none; }
|
.print-wrapper-table { width: 100%; border-collapse: collapse; border: none; }
|
||||||
.print-wrapper-table > thead > tr > td,
|
.print-wrapper-table > thead > tr > td,
|
||||||
.print-wrapper-table > tbody > tr > td { padding: 0; border: none; background: none; }
|
.print-wrapper-table > tbody > tr > td { padding: 0; border: none; background: none; }
|
||||||
@@ -664,7 +929,15 @@ export default function useAttendanceAdmin({ alert }: AlertContext) {
|
|||||||
formData: ShiftFormData,
|
formData: ShiftFormData,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const totalWork = calcFormWorkMinutes(formData);
|
const totalWork = calcFormWorkMinutes(formData);
|
||||||
const totalProject = calcProjectMinutesTotal(logs);
|
const totalProject = calcProjectMinutesTotal(
|
||||||
|
logs.map((l) => ({
|
||||||
|
...l,
|
||||||
|
project_id:
|
||||||
|
l.project_id !== "" && l.project_id != null
|
||||||
|
? Number(l.project_id)
|
||||||
|
: undefined,
|
||||||
|
})),
|
||||||
|
);
|
||||||
if (totalWork > 0 && totalProject !== totalWork) {
|
if (totalWork > 0 && totalProject !== totalWork) {
|
||||||
const wH = Math.floor(totalWork / 60);
|
const wH = Math.floor(totalWork / 60);
|
||||||
const wM = totalWork % 60;
|
const wM = totalWork % 60;
|
||||||
@@ -869,7 +1142,7 @@ export default function useAttendanceAdmin({ alert }: AlertContext) {
|
|||||||
const userName = record.users
|
const userName = record.users
|
||||||
? `${record.users.first_name} ${record.users.last_name}`.trim() ||
|
? `${record.users.first_name} ${record.users.last_name}`.trim() ||
|
||||||
record.users.username
|
record.users.username
|
||||||
: ((record as Record<string, unknown>).user_name as string) ||
|
: ((record as unknown as Record<string, unknown>).user_name as string) ||
|
||||||
`User #${record.user_id}`;
|
`User #${record.user_id}`;
|
||||||
const enriched = { ...record, user_name: userName };
|
const enriched = { ...record, user_name: userName };
|
||||||
setEditingRecord(enriched);
|
setEditingRecord(enriched);
|
||||||
|
|||||||
527
src/admin/hooks/usePlanWork.ts
Normal file
527
src/admin/hooks/usePlanWork.ts
Normal file
@@ -0,0 +1,527 @@
|
|||||||
|
import { useState, useCallback, useMemo } from "react";
|
||||||
|
import {
|
||||||
|
useQuery,
|
||||||
|
useQueryClient,
|
||||||
|
useMutation,
|
||||||
|
keepPreviousData,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
|
import apiFetch from "../utils/api";
|
||||||
|
import { planKeys, GridData, ResolvedCell } from "../lib/queries/plan";
|
||||||
|
|
||||||
|
export type ViewMode = "week" | "month";
|
||||||
|
export type ModalMode =
|
||||||
|
| { kind: "closed" }
|
||||||
|
| { kind: "create"; userId: number; date: string }
|
||||||
|
| { kind: "edit-range"; entryId: number; userId: number; date: string }
|
||||||
|
| { kind: "edit-override"; overrideId: number; userId: number; date: string }
|
||||||
|
| { kind: "day-in-range"; userId: number; date: string; entryId: number }
|
||||||
|
| { kind: "view"; userId: number; date: string };
|
||||||
|
|
||||||
|
function isoDate(d: Date): string {
|
||||||
|
return d.toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startOfWeek(d: Date): Date {
|
||||||
|
const day = d.getUTCDay(); // 0 = Sun
|
||||||
|
const diff = day === 0 ? -6 : 1 - day; // Monday-start
|
||||||
|
const r = new Date(d);
|
||||||
|
r.setUTCDate(d.getUTCDate() + diff);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDays(d: Date, n: number): Date {
|
||||||
|
const r = new Date(d);
|
||||||
|
r.setUTCDate(d.getUTCDate() + n);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apiCall(
|
||||||
|
url: string,
|
||||||
|
options: RequestInit = {},
|
||||||
|
): Promise<unknown> {
|
||||||
|
const res = await apiFetch(url, options);
|
||||||
|
if (!res.ok) {
|
||||||
|
let message = "Chyba serveru";
|
||||||
|
try {
|
||||||
|
const body = await res.json();
|
||||||
|
if (body && typeof body.error === "string") message = body.error;
|
||||||
|
} catch {
|
||||||
|
// body wasn't JSON; use default
|
||||||
|
}
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return await res.json();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsePlanWorkArgs {
|
||||||
|
initialDate?: Date;
|
||||||
|
canEdit: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePlanWork({ initialDate, canEdit }: UsePlanWorkArgs) {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
const [view, setView] = useState<ViewMode>("week");
|
||||||
|
const [anchor, setAnchor] = useState<Date>(initialDate ?? new Date());
|
||||||
|
const [filterActive, setFilterActive] = useState(true);
|
||||||
|
const [modal, setModal] = useState<ModalMode>({ kind: "closed" });
|
||||||
|
|
||||||
|
const range = useMemo(() => {
|
||||||
|
if (view === "week") {
|
||||||
|
const from = startOfWeek(anchor);
|
||||||
|
const to = addDays(from, 6);
|
||||||
|
return { from, to };
|
||||||
|
}
|
||||||
|
const from = new Date(
|
||||||
|
Date.UTC(anchor.getUTCFullYear(), anchor.getUTCMonth(), 1),
|
||||||
|
);
|
||||||
|
const to = new Date(
|
||||||
|
Date.UTC(anchor.getUTCFullYear(), anchor.getUTCMonth() + 1, 0),
|
||||||
|
);
|
||||||
|
return { from, to };
|
||||||
|
}, [view, anchor]);
|
||||||
|
|
||||||
|
const gridQuery = useQuery({
|
||||||
|
queryKey: planKeys.grid(isoDate(range.from), isoDate(range.to), view),
|
||||||
|
queryFn: () =>
|
||||||
|
apiFetch(
|
||||||
|
`/api/admin/plan/grid?date_from=${isoDate(range.from)}&date_to=${isoDate(range.to)}&view=${view}`,
|
||||||
|
).then((r) => r.json().then((b: any) => b.data as GridData)),
|
||||||
|
// keepPreviousData holds the old range's data visible while the new
|
||||||
|
// range's fetch is in flight. Without this, `gridQuery.data` is
|
||||||
|
// immediately undefined on key change and the grid flashes empty.
|
||||||
|
// `isPlaceholderData` is true while the displayed data is stale.
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const invalidate = useCallback(() => {
|
||||||
|
qc.invalidateQueries({ queryKey: planKeys.all });
|
||||||
|
// Plan mutations write audit-log rows, so they touch the audit domain
|
||||||
|
// too. Invalidate the dashboard activity feed and the audit-log page so
|
||||||
|
// they reflect the change without a manual F5 (prefix-matches all
|
||||||
|
// ["audit-log", {...}] filter variants). The dashboard query is active
|
||||||
|
// when visible and refetches immediately despite its staleTime.
|
||||||
|
qc.invalidateQueries({ queryKey: ["dashboard"] });
|
||||||
|
qc.invalidateQueries({ queryKey: ["audit-log"] });
|
||||||
|
}, [qc]);
|
||||||
|
|
||||||
|
// --- Optimistic cache patches ---------------------------------------------
|
||||||
|
// After a mutation succeeds, we patch the visible grid query's data
|
||||||
|
// *before* the refetch resolves. This is what stops the full-grid
|
||||||
|
// re-animation: the visible data updates in place (and the new cell
|
||||||
|
// gets a one-shot pulse via PlanWork's lastMutated state), so the grid
|
||||||
|
// never unmounts. The subsequent invalidate+refetch confirms the patch
|
||||||
|
// from the server (which is the source of truth) and is a no-op if the
|
||||||
|
// server's data matches our patch.
|
||||||
|
//
|
||||||
|
// If the mutation fails, the mutation's onError handler in PlanWork
|
||||||
|
// rolls the cache back to the snapshot we took before the patch.
|
||||||
|
|
||||||
|
const currentGridKey = planKeys.grid(
|
||||||
|
isoDate(range.from),
|
||||||
|
isoDate(range.to),
|
||||||
|
view,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Walk the days in [from, to] inclusive (YYYY-MM-DD strings). Used to
|
||||||
|
// figure out which cell keys to patch for a range entry.
|
||||||
|
function eachDay(from: string, to: string): string[] {
|
||||||
|
const out: string[] = [];
|
||||||
|
const a = new Date(from + "T00:00:00.000Z");
|
||||||
|
const b = new Date(to + "T00:00:00.000Z");
|
||||||
|
for (let d = new Date(a); d <= b; d.setUTCDate(d.getUTCDate() + 1)) {
|
||||||
|
out.push(d.toISOString().slice(0, 10));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot the currently-cached grid for a given key. Returns
|
||||||
|
// `undefined` if there's nothing cached (we don't patch empty data).
|
||||||
|
function snapshotGrid(key: readonly unknown[]): GridData | undefined {
|
||||||
|
return qc.getQueryData<GridData>(key as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply a per-date cell patch to the cached grid at `key`. Returns the
|
||||||
|
// previous cells[userId] map so the caller can roll back on error.
|
||||||
|
function patchCells(
|
||||||
|
key: readonly unknown[],
|
||||||
|
userId: number,
|
||||||
|
dates: string[],
|
||||||
|
mutator: (prev: ResolvedCell | null) => ResolvedCell | null,
|
||||||
|
): Record<string, ResolvedCell | null> | null {
|
||||||
|
const prev = snapshotGrid(key);
|
||||||
|
if (!prev) return null;
|
||||||
|
const userPrev = prev.cells[userId] ?? {};
|
||||||
|
const rolled: Record<string, ResolvedCell | null> = {};
|
||||||
|
const userNext: Record<string, ResolvedCell | null> = { ...userPrev };
|
||||||
|
for (const date of dates) {
|
||||||
|
rolled[date] = userPrev[date] ?? null;
|
||||||
|
userNext[date] = mutator(rolled[date]);
|
||||||
|
}
|
||||||
|
qc.setQueryData(key, {
|
||||||
|
...prev,
|
||||||
|
cells: { ...prev.cells, [userId]: userNext },
|
||||||
|
});
|
||||||
|
return rolled;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a ResolvedCell for a freshly-created entry. The server returns
|
||||||
|
// a row with the same fields, so we mirror that shape. We don't have
|
||||||
|
// entryId/overrideId from the request body — but the createEntry
|
||||||
|
// response *does* include id; we could plumb it through. For now we
|
||||||
|
// leave entryId as a placeholder that the refetch will replace.
|
||||||
|
// (PlanGrid only uses source/entryId/overrideId to decide edit flow;
|
||||||
|
// a fresh cell from an entry is "entry" source — the refetch fills
|
||||||
|
// the actual id within ~200ms.)
|
||||||
|
function makeEntryCell(args: {
|
||||||
|
userId: number;
|
||||||
|
date: string;
|
||||||
|
projectId: number | null;
|
||||||
|
category: string;
|
||||||
|
note: string;
|
||||||
|
rangeFrom: string;
|
||||||
|
rangeTo: string;
|
||||||
|
entryId: number | null;
|
||||||
|
}): ResolvedCell {
|
||||||
|
return {
|
||||||
|
source: "entry",
|
||||||
|
entryId: args.entryId,
|
||||||
|
overrideId: null,
|
||||||
|
user_id: args.userId,
|
||||||
|
shift_date: args.date,
|
||||||
|
project_id: args.projectId,
|
||||||
|
// Optimistic placeholder — the invalidate+refetch fills the real
|
||||||
|
// project label (resolveGrid embeds it) within ~200ms.
|
||||||
|
project_number: null,
|
||||||
|
project_name: null,
|
||||||
|
category: args.category,
|
||||||
|
note: args.note,
|
||||||
|
rangeFrom: args.rangeFrom,
|
||||||
|
rangeTo: args.rangeTo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeOverrideCell(args: {
|
||||||
|
userId: number;
|
||||||
|
date: string;
|
||||||
|
projectId: number | null;
|
||||||
|
category: string;
|
||||||
|
note: string;
|
||||||
|
overrideId: number | null;
|
||||||
|
}): ResolvedCell {
|
||||||
|
return {
|
||||||
|
source: "override",
|
||||||
|
entryId: null,
|
||||||
|
overrideId: args.overrideId,
|
||||||
|
user_id: args.userId,
|
||||||
|
shift_date: args.date,
|
||||||
|
project_id: args.projectId,
|
||||||
|
// Optimistic placeholder — refetch fills the real project label.
|
||||||
|
project_number: null,
|
||||||
|
project_name: null,
|
||||||
|
category: args.category,
|
||||||
|
note: args.note,
|
||||||
|
rangeFrom: null,
|
||||||
|
rangeTo: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Mutations ---
|
||||||
|
|
||||||
|
const createEntry = useMutation({
|
||||||
|
mutationFn: (body: any) =>
|
||||||
|
apiCall("/api/admin/plan/entries", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}),
|
||||||
|
onSuccess: (data: any, body: any) => {
|
||||||
|
// Patch the visible grid with the new entry. We use the response
|
||||||
|
// id (server-assigned) for the new entry's entryId; the rest of
|
||||||
|
// the ResolvedCell mirrors the request body.
|
||||||
|
const days = eachDay(body.date_from, body.date_to);
|
||||||
|
const id = data && typeof data.id === "number" ? data.id : null;
|
||||||
|
const rolled = patchCells(currentGridKey, body.user_id, days, () =>
|
||||||
|
makeEntryCell({
|
||||||
|
userId: body.user_id,
|
||||||
|
date: days[0],
|
||||||
|
projectId: body.project_id ?? null,
|
||||||
|
category: body.category,
|
||||||
|
note: body.note,
|
||||||
|
rangeFrom: body.date_from,
|
||||||
|
rangeTo: body.date_to,
|
||||||
|
entryId: id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// Stash the rollback on the mutation object so PlanWork can call
|
||||||
|
// it from onError.
|
||||||
|
(createEntry as any)._rolled = rolled;
|
||||||
|
invalidate();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateEntry = useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
id,
|
||||||
|
body,
|
||||||
|
force,
|
||||||
|
}: {
|
||||||
|
id: number;
|
||||||
|
body: any;
|
||||||
|
force?: boolean;
|
||||||
|
}) =>
|
||||||
|
apiCall(`/api/admin/plan/entries/${id}${force ? "?force=1" : ""}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}),
|
||||||
|
onSuccess: (_data, vars) => {
|
||||||
|
// We don't know the new full range without the response, but we
|
||||||
|
// do have the body's date_from/date_to. If those are present,
|
||||||
|
// patch the new range. Old cells outside the new range are NOT
|
||||||
|
// cleared here — they'll either still be valid (date_from/to
|
||||||
|
// were partial updates) or the refetch will fix them.
|
||||||
|
const { id, body } = vars;
|
||||||
|
if (body.date_from && body.date_to) {
|
||||||
|
const days = eachDay(body.date_from, body.date_to);
|
||||||
|
// Find the user that owns this entry in the current grid by
|
||||||
|
// looking for any cell with entryId === id (we already know
|
||||||
|
// the id from vars; it doesn't change across the update).
|
||||||
|
const grid = qc.getQueryData<GridData>(currentGridKey as any);
|
||||||
|
let ownerUserId: number | null = null;
|
||||||
|
if (grid) {
|
||||||
|
for (const [uidStr, byDate] of Object.entries(grid.cells)) {
|
||||||
|
for (const cell of Object.values(byDate)) {
|
||||||
|
if (cell && cell.entryId === id) {
|
||||||
|
ownerUserId = Number(uidStr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ownerUserId !== null) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ownerUserId !== null) {
|
||||||
|
const rolled = patchCells(currentGridKey, ownerUserId, days, (prev) =>
|
||||||
|
makeEntryCell({
|
||||||
|
userId: ownerUserId!,
|
||||||
|
date: days[0],
|
||||||
|
projectId:
|
||||||
|
body.project_id === undefined
|
||||||
|
? (prev?.project_id ?? null)
|
||||||
|
: body.project_id,
|
||||||
|
category: body.category ?? prev?.category ?? "work",
|
||||||
|
note: body.note ?? prev?.note ?? "",
|
||||||
|
rangeFrom: body.date_from,
|
||||||
|
rangeTo: body.date_to,
|
||||||
|
entryId: id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
(updateEntry as any)._rolled = rolled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
invalidate();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteEntry = useMutation({
|
||||||
|
mutationFn: ({ id, force }: { id: number; force?: boolean }) =>
|
||||||
|
apiCall(`/api/admin/plan/entries/${id}${force ? "?force=1" : ""}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
}),
|
||||||
|
onSuccess: (_data, vars) => {
|
||||||
|
// For delete we need to know the entry's user_id and full range.
|
||||||
|
// Look it up from the current grid: find the user that has a cell
|
||||||
|
// with entryId === id, and read rangeFrom/rangeTo from that cell.
|
||||||
|
const grid = qc.getQueryData<GridData>(currentGridKey as any);
|
||||||
|
if (grid) {
|
||||||
|
for (const [uidStr, byDate] of Object.entries(grid.cells)) {
|
||||||
|
for (const [, cell] of Object.entries(byDate)) {
|
||||||
|
if (
|
||||||
|
cell &&
|
||||||
|
cell.entryId === vars.id &&
|
||||||
|
cell.rangeFrom &&
|
||||||
|
cell.rangeTo
|
||||||
|
) {
|
||||||
|
const days = eachDay(cell.rangeFrom, cell.rangeTo);
|
||||||
|
const rolled = patchCells(
|
||||||
|
currentGridKey,
|
||||||
|
Number(uidStr),
|
||||||
|
days,
|
||||||
|
() => null,
|
||||||
|
);
|
||||||
|
(deleteEntry as any)._rolled = rolled;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
invalidate();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const createOverride = useMutation({
|
||||||
|
mutationFn: (body: any) =>
|
||||||
|
apiCall("/api/admin/plan/overrides", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}),
|
||||||
|
onSuccess: (data: any, vars: any) => {
|
||||||
|
const id = data && typeof data.id === "number" ? data.id : null;
|
||||||
|
const rolled = patchCells(
|
||||||
|
currentGridKey,
|
||||||
|
vars.user_id,
|
||||||
|
[vars.shift_date],
|
||||||
|
() =>
|
||||||
|
makeOverrideCell({
|
||||||
|
userId: vars.user_id,
|
||||||
|
date: vars.shift_date,
|
||||||
|
projectId: vars.project_id ?? null,
|
||||||
|
category: vars.category,
|
||||||
|
note: vars.note,
|
||||||
|
overrideId: id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
(createOverride as any)._rolled = rolled;
|
||||||
|
invalidate();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateOverride = useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
id,
|
||||||
|
body,
|
||||||
|
force,
|
||||||
|
}: {
|
||||||
|
id: number;
|
||||||
|
body: any;
|
||||||
|
force?: boolean;
|
||||||
|
}) =>
|
||||||
|
apiCall(`/api/admin/plan/overrides/${id}${force ? "?force=1" : ""}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}),
|
||||||
|
onSuccess: (_data, vars) => {
|
||||||
|
// Find the user/date for this overrideId in the current grid, then
|
||||||
|
// patch that single cell with the new values.
|
||||||
|
const grid = qc.getQueryData<GridData>(currentGridKey as any);
|
||||||
|
if (grid) {
|
||||||
|
for (const [uidStr, byDate] of Object.entries(grid.cells)) {
|
||||||
|
for (const [date, cell] of Object.entries(byDate)) {
|
||||||
|
if (cell && cell.overrideId === vars.id) {
|
||||||
|
const rolled = patchCells(
|
||||||
|
currentGridKey,
|
||||||
|
Number(uidStr),
|
||||||
|
[date],
|
||||||
|
(prev) =>
|
||||||
|
makeOverrideCell({
|
||||||
|
userId: Number(uidStr),
|
||||||
|
date,
|
||||||
|
projectId: vars.body.project_id ?? prev?.project_id ?? null,
|
||||||
|
category: vars.body.category ?? prev?.category ?? "work",
|
||||||
|
note: vars.body.note ?? prev?.note ?? "",
|
||||||
|
overrideId: vars.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
(updateOverride as any)._rolled = rolled;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
invalidate();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteOverride = useMutation({
|
||||||
|
mutationFn: ({ id, force }: { id: number; force?: boolean }) =>
|
||||||
|
apiCall(`/api/admin/plan/overrides/${id}${force ? "?force=1" : ""}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
}),
|
||||||
|
onSuccess: (_data, vars) => {
|
||||||
|
const grid = qc.getQueryData<GridData>(currentGridKey as any);
|
||||||
|
if (grid) {
|
||||||
|
for (const [uidStr, byDate] of Object.entries(grid.cells)) {
|
||||||
|
for (const [date, cell] of Object.entries(byDate)) {
|
||||||
|
if (cell && cell.overrideId === vars.id) {
|
||||||
|
const rolled = patchCells(
|
||||||
|
currentGridKey,
|
||||||
|
Number(uidStr),
|
||||||
|
[date],
|
||||||
|
() => null,
|
||||||
|
);
|
||||||
|
(deleteOverride as any)._rolled = rolled;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
invalidate();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Roll back an optimistic patch on mutation error. Called from
|
||||||
|
// PlanWork's mutation wrappers via `rollbackMutation(mutation, key)`.
|
||||||
|
// `mutation` is a TanStack mutation result with a `_rolled` snapshot stashed
|
||||||
|
// on it (see the `(createEntry as any)._rolled = …` writes above). Typed as
|
||||||
|
// `unknown` + an explicit cast so callers can pass the mutation object
|
||||||
|
// directly without the "weak type" mismatch a `{ _rolled? }` param causes.
|
||||||
|
function rollbackMutation(mutation: unknown, userId: number) {
|
||||||
|
const stash = mutation as {
|
||||||
|
_rolled?: Record<string, ResolvedCell | null> | null;
|
||||||
|
};
|
||||||
|
if (!stash._rolled) return;
|
||||||
|
const prev = qc.getQueryData<GridData>(currentGridKey as any);
|
||||||
|
if (!prev) return;
|
||||||
|
const userPrev = prev.cells[userId] ?? {};
|
||||||
|
const userNext = { ...userPrev };
|
||||||
|
for (const [date, cell] of Object.entries(stash._rolled)) {
|
||||||
|
userNext[date] = cell;
|
||||||
|
}
|
||||||
|
qc.setQueryData(currentGridKey, {
|
||||||
|
...prev,
|
||||||
|
cells: { ...prev.cells, [userId]: userNext },
|
||||||
|
});
|
||||||
|
stash._rolled = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
view,
|
||||||
|
setView,
|
||||||
|
anchor,
|
||||||
|
setAnchor,
|
||||||
|
filterActive,
|
||||||
|
setFilterActive,
|
||||||
|
range,
|
||||||
|
grid: gridQuery.data,
|
||||||
|
gridLoading: gridQuery.isLoading,
|
||||||
|
gridFetching: gridQuery.isFetching,
|
||||||
|
gridIsPlaceholder: gridQuery.isPlaceholderData,
|
||||||
|
modal,
|
||||||
|
setModal,
|
||||||
|
canEdit,
|
||||||
|
createEntry,
|
||||||
|
updateEntry,
|
||||||
|
deleteEntry,
|
||||||
|
createOverride,
|
||||||
|
updateOverride,
|
||||||
|
deleteOverride,
|
||||||
|
invalidate,
|
||||||
|
// Exposed so PlanWork can roll back an optimistic patch when a
|
||||||
|
// mutation throws. Pass the failed mutation and the userId it
|
||||||
|
// targeted.
|
||||||
|
rollbackMutation,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCell(
|
||||||
|
grid: GridData | undefined,
|
||||||
|
userId: number,
|
||||||
|
date: string,
|
||||||
|
): ResolvedCell | null {
|
||||||
|
return grid?.cells?.[userId]?.[date] ?? null;
|
||||||
|
}
|
||||||
24
src/admin/hooks/useReducedMotion.ts
Normal file
24
src/admin/hooks/useReducedMotion.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when the user has expressed a preference for reduced motion
|
||||||
|
* (OS-level setting: `prefers-reduced-motion: reduce`).
|
||||||
|
*
|
||||||
|
* SSR-safe: starts as `false` (the default state), so the first render uses
|
||||||
|
* the normal animation. The effect then reconciles with the live matchMedia
|
||||||
|
* state on the client and re-renders if needed.
|
||||||
|
*/
|
||||||
|
export default function useReducedMotion(): boolean {
|
||||||
|
const [reduced, setReduced] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === "undefined" || !window.matchMedia) return;
|
||||||
|
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||||||
|
setReduced(mq.matches);
|
||||||
|
const onChange = () => setReduced(mq.matches);
|
||||||
|
mq.addEventListener("change", onChange);
|
||||||
|
return () => mq.removeEventListener("change", onChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return reduced;
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@ export interface InvoiceItem {
|
|||||||
quantity: number;
|
quantity: number;
|
||||||
unit: string;
|
unit: string;
|
||||||
unit_price: number;
|
unit_price: number;
|
||||||
|
vat_rate?: number;
|
||||||
is_included_in_total: boolean;
|
is_included_in_total: boolean;
|
||||||
position?: number;
|
position?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
113
src/admin/lib/queries/mutations.ts
Normal file
113
src/admin/lib/queries/mutations.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import {
|
||||||
|
useMutation,
|
||||||
|
useQueryClient,
|
||||||
|
type UseMutationOptions,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
|
import apiFetch from "../../utils/api";
|
||||||
|
|
||||||
|
export interface ApiError extends Error {
|
||||||
|
status?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HttpMethod = "POST" | "PUT" | "PATCH" | "DELETE";
|
||||||
|
|
||||||
|
interface ApiResponseBody<T> {
|
||||||
|
success: boolean;
|
||||||
|
data?: T;
|
||||||
|
error?: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performMutation<TIn, TOut>(opts: {
|
||||||
|
url: string | ((input: TIn) => string);
|
||||||
|
method: HttpMethod | ((input: TIn) => HttpMethod);
|
||||||
|
body?: TIn;
|
||||||
|
}): Promise<TOut> {
|
||||||
|
const url =
|
||||||
|
typeof opts.url === "function" ? opts.url(opts.body as TIn) : opts.url;
|
||||||
|
const method =
|
||||||
|
typeof opts.method === "function"
|
||||||
|
? opts.method(opts.body as TIn)
|
||||||
|
: opts.method;
|
||||||
|
const response = await apiFetch(url, {
|
||||||
|
method,
|
||||||
|
headers:
|
||||||
|
opts.body !== undefined
|
||||||
|
? { "Content-Type": "application/json" }
|
||||||
|
: undefined,
|
||||||
|
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
const err: ApiError = new Error("Unauthorized");
|
||||||
|
err.status = 401;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: ApiResponseBody<TOut>;
|
||||||
|
try {
|
||||||
|
result = (await response.json()) as ApiResponseBody<TOut>;
|
||||||
|
} catch {
|
||||||
|
throw new Error("Invalid JSON response");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok || !result.success) {
|
||||||
|
const err: ApiError = new Error(
|
||||||
|
result.error || `Request failed (${response.status})`,
|
||||||
|
);
|
||||||
|
err.status = response.status;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data as TOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiMutationOptions<TIn, TOut> {
|
||||||
|
url: string | ((input: TIn) => string);
|
||||||
|
method: HttpMethod | ((input: TIn) => HttpMethod);
|
||||||
|
/** Query-key prefixes to invalidate on success. Broad per CLAUDE.md. */
|
||||||
|
invalidate?: readonly string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook that wraps `useMutation` with `apiFetch` and broad-invalidation.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* const createCategory = useApiMutation<CategoryForm, WarehouseCategory>({
|
||||||
|
* url: "/api/admin/warehouse/categories",
|
||||||
|
* method: "POST",
|
||||||
|
* invalidate: ["warehouse"],
|
||||||
|
* });
|
||||||
|
* createCategory.mutate(form, {
|
||||||
|
* onSuccess: () => { ... },
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* The returned `data` from a successful mutation is `TOut`. Errors are thrown
|
||||||
|
* as `ApiError` (with `.status` attached) so callers can branch on HTTP code.
|
||||||
|
*/
|
||||||
|
export function useApiMutation<TIn, TOut>(
|
||||||
|
opts: ApiMutationOptions<TIn, TOut> &
|
||||||
|
Omit<UseMutationOptions<TOut, ApiError, TIn>, "mutationFn">,
|
||||||
|
) {
|
||||||
|
const { url, method, invalidate, onSuccess, ...rest } = opts;
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<TOut, ApiError, TIn, unknown>({
|
||||||
|
mutationFn: (input: TIn) =>
|
||||||
|
performMutation<TIn, TOut>({ url, method, body: input }),
|
||||||
|
...rest,
|
||||||
|
onSuccess: (data, variables, onMutateResult, context) => {
|
||||||
|
if (invalidate) {
|
||||||
|
for (const key of invalidate) {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [key] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Forward to user-provided onSuccess with the 4-arg signature expected by TanStack.
|
||||||
|
(
|
||||||
|
onSuccess as
|
||||||
|
| ((d: TOut, v: TIn, r: unknown, c: unknown) => void)
|
||||||
|
| undefined
|
||||||
|
)?.(data, variables, onMutateResult, context);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ export interface ScopeSection {
|
|||||||
export interface ScopeTemplate {
|
export interface ScopeTemplate {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
scope_template_sections?: ScopeSection[];
|
scope_template_sections?: ScopeSection[];
|
||||||
}
|
}
|
||||||
@@ -70,6 +71,8 @@ export const offerListOptions = (filters: {
|
|||||||
order?: string;
|
order?: string;
|
||||||
page?: number;
|
page?: number;
|
||||||
perPage?: number;
|
perPage?: number;
|
||||||
|
status?: string;
|
||||||
|
customer_id?: number;
|
||||||
}) =>
|
}) =>
|
||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ["offers", "list", filters],
|
queryKey: ["offers", "list", filters],
|
||||||
@@ -80,6 +83,9 @@ export const offerListOptions = (filters: {
|
|||||||
if (filters.order) params.set("order", filters.order);
|
if (filters.order) params.set("order", filters.order);
|
||||||
if (filters.page) params.set("page", String(filters.page));
|
if (filters.page) params.set("page", String(filters.page));
|
||||||
if (filters.perPage) params.set("per_page", String(filters.perPage));
|
if (filters.perPage) params.set("per_page", String(filters.perPage));
|
||||||
|
if (filters.status) params.set("status", filters.status);
|
||||||
|
if (filters.customer_id)
|
||||||
|
params.set("customer_id", String(filters.customer_id));
|
||||||
const qs = params.toString();
|
const qs = params.toString();
|
||||||
return paginatedJsonQuery(`/api/admin/offers${qs ? `?${qs}` : ""}`);
|
return paginatedJsonQuery(`/api/admin/offers${qs ? `?${qs}` : ""}`);
|
||||||
},
|
},
|
||||||
@@ -127,9 +133,6 @@ export interface OfferDetailData {
|
|||||||
language: string;
|
language: string;
|
||||||
vat_rate: number;
|
vat_rate: number;
|
||||||
apply_vat: boolean;
|
apply_vat: boolean;
|
||||||
exchange_rate: string;
|
|
||||||
scope_title: string;
|
|
||||||
scope_description: string;
|
|
||||||
items?: OfferItemData[];
|
items?: OfferItemData[];
|
||||||
sections?: OfferSectionData[];
|
sections?: OfferSectionData[];
|
||||||
status: string;
|
status: string;
|
||||||
@@ -142,6 +145,9 @@ export const offerDetailOptions = (id: string | undefined) =>
|
|||||||
queryKey: ["offers", id],
|
queryKey: ["offers", id],
|
||||||
queryFn: () => jsonQuery<OfferDetailData>(`/api/admin/offers/${id}`),
|
queryFn: () => jsonQuery<OfferDetailData>(`/api/admin/offers/${id}`),
|
||||||
enabled: !!id,
|
enabled: !!id,
|
||||||
|
// 404s (deleted/missing offers) are not transient. Retrying just spams
|
||||||
|
// GETs and fires the detail-page redirect toast repeatedly.
|
||||||
|
retry: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const offerNextNumberOptions = () =>
|
export const offerNextNumberOptions = () =>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user