Compare commits
13 Commits
42029533fd
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e79bbbfcc4 | |||
| 2dd7d33c8b | |||
| 13362528cd | |||
| c1b24206b9 | |||
| 560b3fe44e | |||
| 1441a14b90 | |||
| 2032725563 | |||
| c564608983 | |||
| 6e56cfc9cd | |||
| 6e64b97150 | |||
| 249a4b455e | |||
| 0544d41b54 | |||
| 860ba21dd0 |
@@ -0,0 +1,243 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Project Overview
|
||||
|
||||
`ewa-mobile` is a Vue 3 + Vite + Vant frontend with a Tauri 2 Rust backend.
|
||||
|
||||
The app uses local and remote model resources exposed through `che-tauri`, which is built on top of `che-orm`. Frontend model access should go through the generated TypeScript API in `src/generated`.
|
||||
|
||||
## Main Stack
|
||||
|
||||
- Vue 3 with `<script setup>` single-file components.
|
||||
- TypeScript with strict checks enabled.
|
||||
- Vite for frontend dev/build.
|
||||
- Vant for mobile UI components.
|
||||
- Vue Router with hash history.
|
||||
- Tauri 2 backend in `src-tauri`.
|
||||
- SQLite local storage through `che-orm`.
|
||||
- Remote REST integration through `che-tauri` and `reqwest`.
|
||||
|
||||
## Important Paths
|
||||
|
||||
Frontend:
|
||||
|
||||
- `package.json`: npm scripts and frontend dependencies.
|
||||
- `vite.config.ts`: Vite/Tauri dev server config.
|
||||
- `tsconfig.json`: strict TypeScript config.
|
||||
- `src/main.ts`: Vue app bootstrap.
|
||||
- `src/App.vue`: shell layout, nav bar, tabbar.
|
||||
- `src/app/router/index.ts`: main router and auth guard.
|
||||
- `src/apps/auth`: login feature.
|
||||
- `src/apps/tasks`: task list/detail/create feature.
|
||||
- `src/apps/contracts`: contract list/detail/PDF feature.
|
||||
- `src/apps/users`: users feature.
|
||||
- `src/apps/personnel`: personnel shared UI.
|
||||
- `src/shared/auth/useAuth.ts`: frontend auth state and token restore.
|
||||
- `src/shared/composables/useModelApi.ts`: shared model API loading helper.
|
||||
|
||||
Generated frontend API:
|
||||
|
||||
- `src/generated/api_client.ts`
|
||||
- `src/generated/api.ts`
|
||||
- `src/generated/models.ts`
|
||||
|
||||
Tauri backend:
|
||||
|
||||
- `src-tauri/Cargo.toml`: Rust backend dependencies.
|
||||
- `src-tauri/tauri.conf.json`: Tauri app config.
|
||||
- `src-tauri/app.toml`: database and remote API config.
|
||||
- `src-tauri/src/lib.rs`: Tauri commands and API initialization.
|
||||
- `src-tauri/src/main.rs`: native entry point.
|
||||
- `src-tauri/src/bin/manage.rs`: management CLI entry point.
|
||||
- `src-tauri/src/apps/mod.rs`: installed app list.
|
||||
- `src-tauri/src/sync.rs`: contract sync and file download logic.
|
||||
|
||||
## Installed Backend Apps
|
||||
|
||||
Current `che-tauri` app modules:
|
||||
|
||||
- `users`
|
||||
- `frcapp`
|
||||
- `projectapp`
|
||||
- `personemanagment`
|
||||
- `contractapp`
|
||||
|
||||
Each app usually contains:
|
||||
|
||||
- `models.rs`: `che_orm::Model` structs.
|
||||
- `serializers.rs`: frontend/remote field mapping.
|
||||
- `filters.rs`: list filter metadata and remote query mapping.
|
||||
- `mod.rs`: resource registration.
|
||||
- `migrations/`: local SQLite migration SQL and `schema.json` when applicable.
|
||||
|
||||
## Commands
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
Frontend development:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Frontend build/typecheck:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Tauri development:
|
||||
|
||||
```bash
|
||||
npm run tauri dev
|
||||
```
|
||||
|
||||
Tauri build:
|
||||
|
||||
```bash
|
||||
npm run tauri build
|
||||
```
|
||||
|
||||
Backend checks:
|
||||
|
||||
```bash
|
||||
cd src-tauri
|
||||
cargo fmt
|
||||
cargo check
|
||||
```
|
||||
|
||||
Management CLI from `src-tauri`:
|
||||
|
||||
```bash
|
||||
cd src-tauri
|
||||
cargo run --bin manage -- migrate
|
||||
cargo run --bin manage -- generate-ts --out ../src/generated
|
||||
```
|
||||
|
||||
Create or update migrations for one backend app:
|
||||
|
||||
```bash
|
||||
cd src-tauri
|
||||
cargo run --bin manage -- makemigrations <app>
|
||||
cargo run --bin manage -- migrate <app>
|
||||
```
|
||||
|
||||
## Generated API Rules
|
||||
|
||||
- Do not manually edit files under `src/generated` unless explicitly requested.
|
||||
- Generated API is produced from Rust app/module metadata.
|
||||
- After changing backend models, serializers, filters, or resources, regenerate TypeScript:
|
||||
|
||||
```bash
|
||||
cd src-tauri
|
||||
cargo run --bin manage -- generate-ts --out ../src/generated
|
||||
```
|
||||
|
||||
- Frontend code should import model APIs from `src/generated/api`, for example:
|
||||
|
||||
```ts
|
||||
import { taskApi } from "../../../generated/api";
|
||||
```
|
||||
|
||||
- Shared loading/list state should prefer `useModelApi` from `src/shared/composables/useModelApi.ts`.
|
||||
|
||||
## Tauri Commands
|
||||
|
||||
Commands are registered in `src-tauri/src/lib.rs`.
|
||||
|
||||
Important commands:
|
||||
|
||||
- `che_api`: generic model API dispatch used by generated API client.
|
||||
- `auth_login`: remote token login.
|
||||
- `auth_set_token`: restore saved token into backend state.
|
||||
- `auth_logout`: clear backend token.
|
||||
- `auth_status`: backend auth status.
|
||||
- `current_employee`: fetch current employee from remote API.
|
||||
- `sync_contracts`: sync contracts and application files into local SQLite.
|
||||
- `load_application_file`: load a local file or download it from remote URL.
|
||||
|
||||
## Adding Or Changing A Resource
|
||||
|
||||
For local SQLite-backed resources:
|
||||
|
||||
1. Edit `src-tauri/src/apps/<app>/models.rs`.
|
||||
2. Update `serializers.rs`.
|
||||
3. Update `filters.rs` if frontend list filtering needs the field.
|
||||
4. Register or adjust the resource in `<app>/mod.rs`.
|
||||
5. Run `cargo run --bin manage -- makemigrations <app>` from `src-tauri`.
|
||||
6. Run `cargo run --bin manage -- migrate <app>` from `src-tauri` if local DB needs updating.
|
||||
7. Run `cargo run --bin manage -- generate-ts --out ../src/generated`.
|
||||
8. Run `npm run build` from project root.
|
||||
|
||||
For remote-only resources:
|
||||
|
||||
- Use `ctx.remote_resource` or `ctx.mapped_remote_resource` in the app module.
|
||||
- Keep serializer `Field::source(...)` mappings aligned with remote JSON field names.
|
||||
- Keep filters aligned with backend query params and generated frontend list params.
|
||||
- Regenerate TS after resource metadata changes.
|
||||
|
||||
## Frontend Conventions
|
||||
|
||||
- Use Vue 3 `<script setup lang="ts">`.
|
||||
- Keep TypeScript strict-mode clean: no unused locals or parameters.
|
||||
- Prefer existing Vant components and the current mobile layout language.
|
||||
- Preserve current feature grouping under `src/apps/<feature>`.
|
||||
- Use generated API types from `src/generated/models`.
|
||||
- Keep auth-sensitive routes guarded with route metadata and the router guard in `src/app/router/index.ts`.
|
||||
- Do not bypass `useAuth` for login/logout/token restore unless changing auth architecture intentionally.
|
||||
|
||||
## Backend Conventions
|
||||
|
||||
- Keep Rust formatted with `cargo fmt`.
|
||||
- Use `che_orm::Model` for model structs.
|
||||
- Keep model, serializer, filter, resource registration, migration, and generated TS in sync.
|
||||
- Prefer management CLI for migrations instead of hand-writing migration files.
|
||||
- Do not edit old migration files unless the task is specifically migration repair.
|
||||
- Avoid inspecting or modifying `src-tauri/target` and generated Android build artifacts unless the task requires it.
|
||||
|
||||
## Config And Local Data
|
||||
|
||||
- `src-tauri/app.toml` contains database and remote API settings.
|
||||
- `src-tauri/src/lib.rs` currently constructs `AppConfig` inline; do not assume `app.toml` is always the runtime source of truth.
|
||||
- `src-tauri/ewa-mobile.sqlite` is a local SQLite artifact.
|
||||
- Remote API URLs may be environment-specific and may not be reachable on every machine.
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
Frontend-only change:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Tauri backend-only change:
|
||||
|
||||
```bash
|
||||
cd src-tauri
|
||||
cargo fmt
|
||||
cargo check
|
||||
```
|
||||
|
||||
Backend metadata or generated API change:
|
||||
|
||||
```bash
|
||||
cd src-tauri
|
||||
cargo fmt
|
||||
cargo check
|
||||
cargo run --bin manage -- generate-ts --out ../src/generated
|
||||
cd ..
|
||||
npm run build
|
||||
```
|
||||
|
||||
Full app confidence check:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
cd src-tauri
|
||||
cargo fmt
|
||||
cargo check
|
||||
```
|
||||
@@ -1,7 +1,191 @@
|
||||
# Tauri + Vue + TypeScript
|
||||
# ewa-mobile
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
Mobile/desktop EWA client built with Vue 3, Vite, Vant and Tauri 2.
|
||||
|
||||
## Recommended IDE Setup
|
||||
The frontend uses generated TypeScript APIs from the Tauri backend. The backend exposes local and remote model resources through `che-tauri`, backed by `che-orm` and SQLite.
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||
## Stack
|
||||
|
||||
- Vue 3 with `<script setup>`.
|
||||
- TypeScript strict mode.
|
||||
- Vite.
|
||||
- Vue Router with hash history.
|
||||
- Vant mobile UI components.
|
||||
- Tauri 2 Rust backend.
|
||||
- SQLite via `che-orm`.
|
||||
- Remote REST access and sync via `che-tauri`/`reqwest`.
|
||||
|
||||
## Setup
|
||||
|
||||
Install frontend dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
Run frontend dev server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Run the Tauri app in development mode:
|
||||
|
||||
```bash
|
||||
npm run tauri dev
|
||||
```
|
||||
|
||||
Build frontend:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Build Tauri app:
|
||||
|
||||
```bash
|
||||
npm run tauri build
|
||||
```
|
||||
|
||||
## Backend Commands
|
||||
|
||||
Run backend checks:
|
||||
|
||||
```bash
|
||||
cd src-tauri
|
||||
cargo fmt
|
||||
cargo check
|
||||
```
|
||||
|
||||
Apply migrations:
|
||||
|
||||
```bash
|
||||
cd src-tauri
|
||||
cargo run --bin manage -- migrate
|
||||
```
|
||||
|
||||
Generate TypeScript API from Rust metadata:
|
||||
|
||||
```bash
|
||||
cd src-tauri
|
||||
cargo run --bin manage -- generate-ts --out ../src/generated
|
||||
```
|
||||
|
||||
Create and apply migrations for one app:
|
||||
|
||||
```bash
|
||||
cd src-tauri
|
||||
cargo run --bin manage -- makemigrations <app>
|
||||
cargo run --bin manage -- migrate <app>
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```text
|
||||
src/
|
||||
app/router/ Main Vue Router setup and auth guard
|
||||
apps/auth/ Login feature
|
||||
apps/tasks/ Task list/detail/create screens
|
||||
apps/contracts/ Contract screens and PDF preview
|
||||
apps/users/ Users screens
|
||||
apps/personnel/ Personnel shared components
|
||||
generated/ Generated TypeScript API and models
|
||||
shared/auth/ Auth token state and restore logic
|
||||
shared/composables/ Shared model API helpers
|
||||
|
||||
src-tauri/
|
||||
src/lib.rs Tauri commands and API initialization
|
||||
src/sync.rs Contract sync and file download logic
|
||||
src/bin/manage.rs Management CLI entry point
|
||||
src/apps/ che-tauri app modules
|
||||
```
|
||||
|
||||
## Backend App Modules
|
||||
|
||||
Installed modules are registered in `src-tauri/src/apps/mod.rs`:
|
||||
|
||||
- `users`
|
||||
- `frcapp`
|
||||
- `projectapp`
|
||||
- `personemanagment`
|
||||
- `contractapp`
|
||||
|
||||
Each module usually contains `models.rs`, `serializers.rs`, `filters.rs`, `mod.rs`, and optional `migrations/`.
|
||||
|
||||
## Generated API
|
||||
|
||||
Generated files live in `src/generated`:
|
||||
|
||||
- `api_client.ts`
|
||||
- `api.ts`
|
||||
- `models.ts`
|
||||
|
||||
Do not edit generated files manually unless there is a specific reason. After changing backend models, serializers, filters, or resource registration, regenerate them:
|
||||
|
||||
```bash
|
||||
cd src-tauri
|
||||
cargo run --bin manage -- generate-ts --out ../src/generated
|
||||
```
|
||||
|
||||
Frontend code should use generated APIs, for example:
|
||||
|
||||
```ts
|
||||
import { taskApi } from "../../../generated/api";
|
||||
```
|
||||
|
||||
Shared list/detail loading should prefer `src/shared/composables/useModelApi.ts`.
|
||||
|
||||
## Tauri Commands
|
||||
|
||||
Important commands registered in `src-tauri/src/lib.rs`:
|
||||
|
||||
- `che_api`: generic model API dispatch used by generated clients.
|
||||
- `auth_login`: login against remote API.
|
||||
- `auth_set_token`: restore saved auth token into backend state.
|
||||
- `auth_logout`: clear backend token.
|
||||
- `auth_status`: backend auth status.
|
||||
- `current_employee`: fetch current employee from remote API.
|
||||
- `sync_contracts`: sync contracts and application files to local SQLite.
|
||||
- `load_application_file`: load local file bytes or download from remote URL.
|
||||
|
||||
## Configuration
|
||||
|
||||
- `src-tauri/app.toml` contains database and remote API settings.
|
||||
- `src-tauri/src/lib.rs` currently creates `AppConfig` inline, so do not assume `app.toml` is always the runtime source of truth.
|
||||
- `src-tauri/ewa-mobile.sqlite` is a local SQLite database artifact.
|
||||
- Remote API URLs are environment-specific.
|
||||
|
||||
## Validation
|
||||
|
||||
Frontend change:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Backend change:
|
||||
|
||||
```bash
|
||||
cd src-tauri
|
||||
cargo fmt
|
||||
cargo check
|
||||
```
|
||||
|
||||
Backend metadata or generated API change:
|
||||
|
||||
```bash
|
||||
cd src-tauri
|
||||
cargo fmt
|
||||
cargo check
|
||||
cargo run --bin manage -- generate-ts --out ../src/generated
|
||||
cd ..
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Developer Notes
|
||||
|
||||
- Keep frontend code TypeScript-strict and avoid unused locals/parameters.
|
||||
- Keep model, serializer, filter, resource registration, migration and generated TS in sync.
|
||||
- Prefer the management CLI for migrations.
|
||||
- Avoid editing generated TypeScript and old migration files by hand.
|
||||
- Avoid inspecting or modifying `src-tauri/target` and generated Android build artifacts unless needed.
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"pdfjs-dist": "^5.6.205",
|
||||
"vant": "^4.10.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.6.4"
|
||||
@@ -516,6 +517,271 @@
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@napi-rs/canvas": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.100.tgz",
|
||||
"integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"workspaces": [
|
||||
"e2e/*"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@napi-rs/canvas-android-arm64": "0.1.100",
|
||||
"@napi-rs/canvas-darwin-arm64": "0.1.100",
|
||||
"@napi-rs/canvas-darwin-x64": "0.1.100",
|
||||
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100",
|
||||
"@napi-rs/canvas-linux-arm64-gnu": "0.1.100",
|
||||
"@napi-rs/canvas-linux-arm64-musl": "0.1.100",
|
||||
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.100",
|
||||
"@napi-rs/canvas-linux-x64-gnu": "0.1.100",
|
||||
"@napi-rs/canvas-linux-x64-musl": "0.1.100",
|
||||
"@napi-rs/canvas-win32-arm64-msvc": "0.1.100",
|
||||
"@napi-rs/canvas-win32-x64-msvc": "0.1.100"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-android-arm64": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz",
|
||||
"integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-darwin-arm64": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz",
|
||||
"integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-darwin-x64": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz",
|
||||
"integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz",
|
||||
"integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz",
|
||||
"integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz",
|
||||
"integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz",
|
||||
"integrity": "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz",
|
||||
"integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-x64-musl": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz",
|
||||
"integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-win32-arm64-msvc": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz",
|
||||
"integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz",
|
||||
"integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
|
||||
@@ -1551,6 +1817,13 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/node-readable-to-web-readable-stream": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/node-readable-to-web-readable-stream/-/node-readable-to-web-readable-stream-0.4.2.tgz",
|
||||
"integrity": "sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/path-browserify": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
||||
@@ -1558,6 +1831,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pdfjs-dist": {
|
||||
"version": "5.6.205",
|
||||
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.6.205.tgz",
|
||||
"integrity": "sha512-tlUj+2IDa7G1SbvBNN74UHRLJybZDWYom+k6p5KIZl7huBvsA4APi6mKL+zCxd3tLjN5hOOEE9Tv7VdzO88pfg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=20.19.0 || >=22.13.0 || >=24"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@napi-rs/canvas": "^0.1.96",
|
||||
"node-readable-to-web-readable-stream": "^0.4.2"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"pdfjs-dist": "^5.6.205",
|
||||
"vant": "^4.10.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.6.4"
|
||||
|
||||
@@ -278,6 +278,29 @@ version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-rs"
|
||||
version = "1.17.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1"
|
||||
dependencies = [
|
||||
"aws-lc-sys",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-sys"
|
||||
version = "0.43.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cmake",
|
||||
"dunce",
|
||||
"fs_extra",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
@@ -488,6 +511,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
@@ -524,6 +549,12 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "che-orm"
|
||||
version = "0.1.0"
|
||||
@@ -551,7 +582,7 @@ dependencies = [
|
||||
"async-trait",
|
||||
"che-orm",
|
||||
"clap",
|
||||
"reqwest 0.12.28",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
@@ -611,6 +642,15 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
@@ -652,16 +692,6 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
@@ -685,9 +715,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation",
|
||||
"core-graphics-types",
|
||||
"foreign-types 0.5.0",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -698,7 +728,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -1074,15 +1104,6 @@ version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "endi"
|
||||
version = "1.1.1"
|
||||
@@ -1175,7 +1196,9 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"che-orm",
|
||||
"che-tauri",
|
||||
"reqwest 0.12.28",
|
||||
"jni",
|
||||
"reqwest",
|
||||
"rustls-platform-verifier",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
@@ -1254,15 +1277,6 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.5.0"
|
||||
@@ -1270,7 +1284,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||
dependencies = [
|
||||
"foreign-types-macros",
|
||||
"foreign-types-shared 0.3.1",
|
||||
"foreign-types-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1284,12 +1298,6 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.3.1"
|
||||
@@ -1305,6 +1313,12 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs_extra"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.33"
|
||||
@@ -1517,8 +1531,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1528,9 +1544,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi 5.3.0",
|
||||
"wasip2",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1692,25 +1710,6 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http",
|
||||
"indexmap 2.14.0",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
@@ -1853,7 +1852,6 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
@@ -1879,22 +1877,6 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.20"
|
||||
@@ -1913,11 +1895,9 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2199,6 +2179,16 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.103"
|
||||
@@ -2357,6 +2347,12 @@ version = "0.4.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "markup5ever"
|
||||
version = "0.38.0"
|
||||
@@ -2441,23 +2437,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.9.0"
|
||||
@@ -2499,7 +2478,7 @@ dependencies = [
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-traits",
|
||||
"rand",
|
||||
"rand 0.8.7",
|
||||
"smallvec",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -2779,49 +2758,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"cfg-if",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
@@ -3164,6 +3106,62 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror 2.0.19",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"bytes",
|
||||
"getrandom 0.3.4",
|
||||
"lru-slab",
|
||||
"rand 0.9.5",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.19",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
@@ -3192,8 +3190,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3203,7 +3211,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3215,6 +3233,15 @@ dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-window-handle"
|
||||
version = "0.6.2"
|
||||
@@ -3299,46 +3326,6 @@ version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-tls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.13.4"
|
||||
@@ -3353,15 +3340,22 @@ dependencies = [
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
@@ -3400,7 +3394,7 @@ dependencies = [
|
||||
"num-traits",
|
||||
"pkcs1",
|
||||
"pkcs8",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
"signature",
|
||||
"spki",
|
||||
"subtle",
|
||||
@@ -3441,6 +3435,7 @@ version = "0.23.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"once_cell",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
@@ -3448,21 +3443,62 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
|
||||
dependencies = [
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"jni",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier-android"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
@@ -3562,7 +3598,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
@@ -3822,7 +3858,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4043,7 +4079,7 @@ dependencies = [
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"rand",
|
||||
"rand 0.8.7",
|
||||
"rsa",
|
||||
"serde",
|
||||
"sha1",
|
||||
@@ -4081,7 +4117,7 @@ dependencies = [
|
||||
"md-5",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"rand",
|
||||
"rand 0.8.7",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
@@ -4233,27 +4269,6 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration-sys"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "6.2.2"
|
||||
@@ -4275,7 +4290,7 @@ checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"block2",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation",
|
||||
"core-graphics",
|
||||
"crossbeam-channel",
|
||||
"dbus",
|
||||
@@ -4354,7 +4369,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"plist",
|
||||
"raw-window-handle",
|
||||
"reqwest 0.13.4",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
@@ -4718,16 +4733,6 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.4"
|
||||
@@ -5295,6 +5300,16 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web_atoms"
|
||||
version = "0.2.5"
|
||||
@@ -5351,6 +5366,15 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webview2-com"
|
||||
version = "0.38.2"
|
||||
@@ -5546,17 +5570,6 @@ dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
"windows-result 0.4.1",
|
||||
"windows-strings 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
|
||||
@@ -26,4 +26,6 @@ tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
reqwest = { version = "0.13", default-features = false, features = ["json", "query", "rustls"] }
|
||||
jni = "0.21"
|
||||
rustls-platform-verifier = "0.6"
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
url = "sqlite://ewa-mobile.sqlite?mode=rwc"
|
||||
|
||||
[remote]
|
||||
base_url = "http://127.0.0.1:8000"
|
||||
base_url = "http://192.168.0.166:8000"
|
||||
auth_path = "/api-token-auth/"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default"
|
||||
"opener:default",
|
||||
"opener:allow-open-path"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = false
|
||||
@@ -0,0 +1,20 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
key.properties
|
||||
keystore.properties
|
||||
|
||||
/.tauri
|
||||
/tauri.settings.gradle
|
||||
@@ -0,0 +1,6 @@
|
||||
/src/main/**/generated
|
||||
/src/main/jniLibs/**/*.so
|
||||
/src/main/assets/tauri.conf.json
|
||||
/tauri.build.gradle.kts
|
||||
/proguard-tauri.pro
|
||||
/tauri.properties
|
||||
@@ -0,0 +1,104 @@
|
||||
import java.util.Properties
|
||||
import groovy.json.JsonSlurper
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("rust")
|
||||
}
|
||||
|
||||
val tauriProperties = Properties().apply {
|
||||
val propFile = file("tauri.properties")
|
||||
if (propFile.exists()) {
|
||||
propFile.inputStream().use { load(it) }
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 36
|
||||
namespace = "com.che.ewa_mobile"
|
||||
defaultConfig {
|
||||
manifestPlaceholders["usesCleartextTraffic"] = "false"
|
||||
applicationId = "com.che.ewa_mobile"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
||||
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
|
||||
}
|
||||
buildTypes {
|
||||
getByName("debug") {
|
||||
manifestPlaceholders["usesCleartextTraffic"] = "true"
|
||||
isDebuggable = true
|
||||
isJniDebuggable = true
|
||||
isMinifyEnabled = false
|
||||
packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/x86/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
|
||||
}
|
||||
}
|
||||
getByName("release") {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
*fileTree(".") { include("**/*.pro") }
|
||||
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||
.toList().toTypedArray()
|
||||
)
|
||||
}
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url = uri(rustlsPlatformVerifierMavenDir())
|
||||
metadataSources.artifact()
|
||||
}
|
||||
}
|
||||
|
||||
rust {
|
||||
rootDirRel = "../../../"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("rustls:rustls-platform-verifier:latest.release")
|
||||
implementation("androidx.webkit:webkit:1.14.0")
|
||||
implementation("androidx.appcompat:appcompat:1.7.1")
|
||||
implementation("androidx.activity:activity-ktx:1.10.1")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
implementation("androidx.lifecycle:lifecycle-process:2.10.0")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.4")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
|
||||
}
|
||||
|
||||
apply(from = "tauri.build.gradle.kts")
|
||||
|
||||
fun rustlsPlatformVerifierMavenDir(): String {
|
||||
val manifestPath = providers.exec {
|
||||
commandLine(
|
||||
"cargo",
|
||||
"metadata",
|
||||
"--format-version",
|
||||
"1",
|
||||
"--filter-platform",
|
||||
"aarch64-linux-android",
|
||||
"--manifest-path",
|
||||
rootProject.file("../../Cargo.toml").absolutePath,
|
||||
)
|
||||
}.standardOutput.asText.get()
|
||||
|
||||
val metadata = JsonSlurper().parseText(manifestPath) as Map<*, *>
|
||||
val packages = metadata["packages"] as List<*>
|
||||
val rustlsAndroidPackage = packages
|
||||
.map { it as Map<*, *> }
|
||||
.first { it["name"] == "rustls-platform-verifier-android" }
|
||||
val rustlsManifest = file(rustlsAndroidPackage["manifest_path"] as String)
|
||||
|
||||
return File(rustlsManifest.parentFile, "maven").path
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<!-- AndroidTV support -->
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.ewa_mobile"
|
||||
android:usesCleartextTraffic="${usesCleartextTraffic}">
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/main_activity_title"
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<!-- AndroidTV support -->
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.che.ewa_mobile
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
|
||||
class MainActivity : TauriActivity() {
|
||||
companion object {
|
||||
init {
|
||||
System.loadLibrary("ewa_mobile_lib")
|
||||
}
|
||||
|
||||
@JvmStatic external fun initRustlsPlatformVerifier(context: Context)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
initRustlsPlatformVerifier(applicationContext)
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Hello World!"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,6 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.ewa_mobile" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<resources>
|
||||
<string name="app_name">"ewa-mobile"</string>
|
||||
<string name="main_activity_title">"ewa-mobile"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,6 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.ewa_mobile" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<external-path name="my_images" path="." />
|
||||
<cache-path name="my_cache_images" path="." />
|
||||
</paths>
|
||||
@@ -0,0 +1,22 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.0")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("clean").configure {
|
||||
delete("build")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
create("pluginsForCoolKids") {
|
||||
id = "rust"
|
||||
implementationClass = "RustPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(gradleApi())
|
||||
implementation("com.android.tools.build:gradle:8.11.0")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import java.io.File
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.GradleException
|
||||
import org.gradle.api.logging.LogLevel
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
|
||||
open class BuildTask : DefaultTask() {
|
||||
@Input
|
||||
var rootDirRel: String? = null
|
||||
@Input
|
||||
var target: String? = null
|
||||
@Input
|
||||
var release: Boolean? = null
|
||||
|
||||
@TaskAction
|
||||
fun assemble() {
|
||||
val executable = """npm""";
|
||||
try {
|
||||
runTauriCli(executable)
|
||||
} catch (e: Exception) {
|
||||
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||
// Try different Windows-specific extensions
|
||||
val fallbacks = listOf(
|
||||
"$executable.exe",
|
||||
"$executable.cmd",
|
||||
"$executable.bat",
|
||||
)
|
||||
|
||||
var lastException: Exception = e
|
||||
for (fallback in fallbacks) {
|
||||
try {
|
||||
runTauriCli(fallback)
|
||||
return
|
||||
} catch (fallbackException: Exception) {
|
||||
lastException = fallbackException
|
||||
}
|
||||
}
|
||||
throw lastException
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun runTauriCli(executable: String) {
|
||||
val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null")
|
||||
val target = target ?: throw GradleException("target cannot be null")
|
||||
val release = release ?: throw GradleException("release cannot be null")
|
||||
val args = listOf("run", "--", "tauri", "android", "android-studio-script");
|
||||
|
||||
project.exec {
|
||||
workingDir(File(project.projectDir, rootDirRel))
|
||||
executable(executable)
|
||||
args(args)
|
||||
if (project.logger.isEnabled(LogLevel.DEBUG)) {
|
||||
args("-vv")
|
||||
} else if (project.logger.isEnabled(LogLevel.INFO)) {
|
||||
args("-v")
|
||||
}
|
||||
if (release) {
|
||||
args("--release")
|
||||
}
|
||||
args(listOf("--target", target))
|
||||
}.assertNormalExitValue()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import com.android.build.api.dsl.ApplicationExtension
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.get
|
||||
|
||||
const val TASK_GROUP = "rust"
|
||||
|
||||
open class Config {
|
||||
lateinit var rootDirRel: String
|
||||
}
|
||||
|
||||
open class RustPlugin : Plugin<Project> {
|
||||
private lateinit var config: Config
|
||||
|
||||
override fun apply(project: Project) = with(project) {
|
||||
config = extensions.create("rust", Config::class.java)
|
||||
|
||||
val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64");
|
||||
val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList
|
||||
|
||||
val defaultArchList = listOf("arm64", "arm", "x86", "x86_64");
|
||||
val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList
|
||||
|
||||
val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64")
|
||||
|
||||
extensions.configure<ApplicationExtension> {
|
||||
@Suppress("UnstableApiUsage")
|
||||
flavorDimensions.add("abi")
|
||||
productFlavors {
|
||||
create("universal") {
|
||||
dimension = "abi"
|
||||
ndk {
|
||||
abiFilters += abiList
|
||||
}
|
||||
}
|
||||
defaultArchList.forEachIndexed { index, arch ->
|
||||
create(arch) {
|
||||
dimension = "abi"
|
||||
ndk {
|
||||
abiFilters.add(defaultAbiList[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
for (profile in listOf("debug", "release")) {
|
||||
val profileCapitalized = profile.replaceFirstChar { it.uppercase() }
|
||||
val buildTask = tasks.maybeCreate(
|
||||
"rustBuildUniversal$profileCapitalized",
|
||||
DefaultTask::class.java
|
||||
).apply {
|
||||
group = TASK_GROUP
|
||||
description = "Build dynamic library in $profile mode for all targets"
|
||||
}
|
||||
|
||||
tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask)
|
||||
|
||||
for (targetPair in targetsList.withIndex()) {
|
||||
val targetName = targetPair.value
|
||||
val targetArch = archList[targetPair.index]
|
||||
val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() }
|
||||
val targetBuildTask = project.tasks.maybeCreate(
|
||||
"rustBuild$targetArchCapitalized$profileCapitalized",
|
||||
BuildTask::class.java
|
||||
).apply {
|
||||
group = TASK_GROUP
|
||||
description = "Build dynamic library in $profile mode for $targetArch"
|
||||
rootDirRel = config.rootDirRel
|
||||
target = targetName
|
||||
release = profile == "release"
|
||||
}
|
||||
|
||||
buildTask.dependsOn(targetBuildTask)
|
||||
tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn(
|
||||
targetBuildTask
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app"s APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonFinalResIds=false
|
||||
@@ -0,0 +1,6 @@
|
||||
#Tue May 10 19:22:52 CST 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,3 @@
|
||||
include ':app'
|
||||
|
||||
apply from: 'tauri.settings.gradle'
|
||||
@@ -1,13 +1,13 @@
|
||||
use che_tauri::{Filter, FilterSet};
|
||||
|
||||
use super::models::{Contract, ContractApplicationFile, Counterparty};
|
||||
use super::models::{Contract, ContractApplicationFile, ContractCategory, Counterparty};
|
||||
|
||||
static CONTRACTAPP_FILTERS: &[Filter] = &[
|
||||
Filter::exact("id"),
|
||||
Filter::exact("name"),
|
||||
Filter::contains("name"),
|
||||
Filter::contains("name").remote("name_of_product"),
|
||||
Filter::exact("number"),
|
||||
Filter::contains("number"),
|
||||
Filter::contains("number").remote("number"),
|
||||
Filter::exact("date"),
|
||||
Filter::exact("status"),
|
||||
Filter::exact("status_name"),
|
||||
@@ -16,37 +16,50 @@ static CONTRACTAPP_FILTERS: &[Filter] = &[
|
||||
Filter::contains("contract_type"),
|
||||
Filter::exact("category"),
|
||||
Filter::contains("category"),
|
||||
Filter::exact("category_id").remote("category"),
|
||||
Filter::contains("counterparty_name"),
|
||||
Filter::contains("name_of_product"),
|
||||
Filter::exact("counterparty_id"),
|
||||
Filter::exact("project_id"),
|
||||
Filter::exact("frc_id"),
|
||||
Filter::exact("employee_id"),
|
||||
Filter::contains("name_of_product").remote("name_of_product"),
|
||||
Filter::exact("counterparty_id").remote("company"),
|
||||
Filter::exact("project_id").remote("project"),
|
||||
Filter::exact("frc_id").remote("frc"),
|
||||
Filter::exact("employee_id").remote("employee"),
|
||||
];
|
||||
|
||||
static CONTRACT_CATEGORY_FILTERS: &[Filter] = &[
|
||||
Filter::exact("id"),
|
||||
Filter::exact("name"),
|
||||
Filter::contains("name").local_only(),
|
||||
Filter::exact("name_group"),
|
||||
Filter::contains("name_group").local_only(),
|
||||
];
|
||||
|
||||
static COUNTERPARTY_FILTERS: &[Filter] = &[
|
||||
Filter::exact("id"),
|
||||
Filter::exact("name"),
|
||||
Filter::contains("name"),
|
||||
Filter::contains("name").remote("search"),
|
||||
];
|
||||
|
||||
static CONTRACT_APPLICATION_FILE_FILTERS: &[Filter] = &[
|
||||
Filter::exact("id"),
|
||||
Filter::exact("contract_id"),
|
||||
Filter::exact("contract_id").remote("contract"),
|
||||
Filter::exact("name"),
|
||||
Filter::contains("name"),
|
||||
Filter::contains("name").remote("search"),
|
||||
Filter::exact("file_type"),
|
||||
Filter::exact("status"),
|
||||
];
|
||||
|
||||
pub fn contractapp_filterset() -> FilterSet<Contract> {
|
||||
FilterSet::new(CONTRACTAPP_FILTERS)
|
||||
FilterSet::new(CONTRACTAPP_FILTERS).remote_ordering("order_by")
|
||||
}
|
||||
|
||||
pub fn contract_category_filterset() -> FilterSet<ContractCategory> {
|
||||
FilterSet::new(CONTRACT_CATEGORY_FILTERS).remote_ordering("order_by")
|
||||
}
|
||||
|
||||
pub fn counterparty_filterset() -> FilterSet<Counterparty> {
|
||||
FilterSet::new(COUNTERPARTY_FILTERS)
|
||||
FilterSet::new(COUNTERPARTY_FILTERS).remote_ordering("order_by")
|
||||
}
|
||||
|
||||
pub fn contract_application_file_filterset() -> FilterSet<ContractApplicationFile> {
|
||||
FilterSet::new(CONTRACT_APPLICATION_FILE_FILTERS)
|
||||
FilterSet::new(CONTRACT_APPLICATION_FILE_FILTERS).remote_ordering("order_by")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE IF NOT EXISTS contract_category (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
name_group TEXT NOT NULL,
|
||||
template TEXT,
|
||||
is_questionnair BOOLEAN NOT NULL,
|
||||
parent INTEGER,
|
||||
secure_group INTEGER
|
||||
);
|
||||
|
||||
ALTER TABLE contractapp ADD COLUMN category_id INTEGER REFERENCES contract_category(id);
|
||||
@@ -173,6 +173,88 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "contract_category",
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"ty": "integer",
|
||||
"primary_key": true,
|
||||
"nullable": false,
|
||||
"auto": true,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ty": "text",
|
||||
"primary_key": false,
|
||||
"nullable": false,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "name_group",
|
||||
"ty": "text",
|
||||
"primary_key": false,
|
||||
"nullable": false,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "template",
|
||||
"ty": "text",
|
||||
"primary_key": false,
|
||||
"nullable": true,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "is_questionnair",
|
||||
"ty": "boolean",
|
||||
"primary_key": false,
|
||||
"nullable": false,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "parent",
|
||||
"ty": "integer",
|
||||
"primary_key": false,
|
||||
"nullable": true,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "secure_group",
|
||||
"ty": "integer",
|
||||
"primary_key": false,
|
||||
"nullable": true,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "contractapp",
|
||||
"fields": [
|
||||
@@ -242,6 +324,20 @@
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "category_id",
|
||||
"ty": "integer",
|
||||
"primary_key": false,
|
||||
"nullable": true,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": {
|
||||
"table": "contract_category",
|
||||
"column": "id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "amount_total_display",
|
||||
"ty": "text",
|
||||
|
||||
@@ -16,18 +16,27 @@ impl AppModule for ContractappModule {
|
||||
}
|
||||
|
||||
fn init(&self, ctx: &mut ModuleContext) {
|
||||
ctx.resource::<models::Contract>(
|
||||
ctx.cached_mapped_remote_resource::<models::Contract>(
|
||||
"contract",
|
||||
"/api/contract/",
|
||||
serializers::contractapp_serializer(),
|
||||
filters::contractapp_filterset(),
|
||||
);
|
||||
ctx.resource::<models::Counterparty>(
|
||||
ctx.cached_mapped_remote_resource::<models::ContractCategory>(
|
||||
"contract_category",
|
||||
"/api/cont/category/",
|
||||
serializers::contract_category_serializer(),
|
||||
filters::contract_category_filterset(),
|
||||
);
|
||||
ctx.cached_mapped_remote_resource::<models::Counterparty>(
|
||||
"counterparty",
|
||||
"/api/catalog/company/",
|
||||
serializers::counterparty_serializer(),
|
||||
filters::counterparty_filterset(),
|
||||
);
|
||||
ctx.resource::<models::ContractApplicationFile>(
|
||||
ctx.cached_mapped_remote_resource::<models::ContractApplicationFile>(
|
||||
"contract_application_file",
|
||||
"/api/cont/appfile/",
|
||||
serializers::contract_application_file_serializer(),
|
||||
filters::contract_application_file_filterset(),
|
||||
);
|
||||
|
||||
@@ -15,6 +15,10 @@ pub struct Contract {
|
||||
pub absolute_url: String,
|
||||
pub contract_type: String,
|
||||
pub category: String,
|
||||
|
||||
#[field(foreign_key = ContractCategory)]
|
||||
pub category_id: Option<i64>,
|
||||
|
||||
pub amount_total_display: String,
|
||||
pub amount_by_ds: String,
|
||||
pub comment: String,
|
||||
@@ -48,6 +52,20 @@ pub struct Contract {
|
||||
pub employee_id: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Model)]
|
||||
#[model(table = "contract_category")]
|
||||
pub struct ContractCategory {
|
||||
#[field(primary_key)]
|
||||
pub id: i64,
|
||||
|
||||
pub name: String,
|
||||
pub name_group: String,
|
||||
pub template: Option<String>,
|
||||
pub is_questionnair: bool,
|
||||
pub parent: Option<i64>,
|
||||
pub secure_group: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Model)]
|
||||
#[model(table = "counterparty")]
|
||||
pub struct Counterparty {
|
||||
|
||||
@@ -6,24 +6,25 @@ use crate::apps::{
|
||||
projectapp::{models::Project, serializers::project_serializer},
|
||||
};
|
||||
|
||||
use super::models::{Contract, ContractApplicationFile, Counterparty};
|
||||
use super::models::{Contract, ContractApplicationFile, ContractCategory, Counterparty};
|
||||
|
||||
static CONTRACT_FIELDS: &[Field] = &[
|
||||
Field::new("id").read_only(),
|
||||
Field::new("name"),
|
||||
Field::new("number"),
|
||||
Field::new("absolute_url"),
|
||||
Field::new("contract_type"),
|
||||
Field::new("category"),
|
||||
Field::new("amount_total_display"),
|
||||
Field::new("amount_by_ds"),
|
||||
Field::new("absolute_url").source("get_absolute_url"),
|
||||
Field::new("contract_type").source("get_type"),
|
||||
Field::new("category").source("get_category"),
|
||||
Field::new("category_id").required(false).nullable(),
|
||||
Field::new("amount_total_display").source("get_amount_total"),
|
||||
Field::new("amount_by_ds").source("get_amount_by_ds"),
|
||||
Field::new("comment"),
|
||||
Field::new("date"),
|
||||
Field::new("status_name"),
|
||||
Field::new("status_name").source("get_status"),
|
||||
Field::new("status"),
|
||||
Field::new("nds"),
|
||||
Field::new("name_of_product"),
|
||||
Field::new("counterparty_name"),
|
||||
Field::new("counterparty_name").source("counterparty"),
|
||||
Field::new("amount"),
|
||||
Field::new("month_pay").required(false).nullable(),
|
||||
Field::new("avans_pay"),
|
||||
@@ -34,29 +35,51 @@ static CONTRACT_FIELDS: &[Field] = &[
|
||||
Field::new("bill_paid_sum").required(false).nullable(),
|
||||
Field::new("income_total"),
|
||||
Field::new("arrears"),
|
||||
Field::new("counterparty_id").required(false).nullable(),
|
||||
Field::new("project_id").required(false).nullable(),
|
||||
Field::new("frc_id").required(false).nullable(),
|
||||
Field::new("employee_id").required(false).nullable(),
|
||||
Field::new("counterparty_id")
|
||||
.source("company")
|
||||
.required(false)
|
||||
.nullable(),
|
||||
Field::new("project_id")
|
||||
.source("project")
|
||||
.required(false)
|
||||
.nullable(),
|
||||
Field::new("frc_id")
|
||||
.source("frc")
|
||||
.required(false)
|
||||
.nullable(),
|
||||
Field::new("employee_id")
|
||||
.source("get_employee")
|
||||
.required(false)
|
||||
.nullable(),
|
||||
Field::related("category_ref", "category_id", &CATEGORY_RELATION),
|
||||
Field::related("counterparty", "counterparty_id", &COUNTERPARTY_RELATION),
|
||||
Field::related("project", "project_id", &PROJECT_RELATION),
|
||||
Field::related("frc", "frc_id", &FRC_RELATION),
|
||||
Field::related("employee", "employee_id", &EMPLOYEE_RELATION),
|
||||
];
|
||||
|
||||
static CONTRACT_CATEGORY_FIELDS: &[Field] = &[
|
||||
Field::new("id").read_only(),
|
||||
Field::new("name"),
|
||||
Field::new("name_group"),
|
||||
Field::new("template").required(false).nullable(),
|
||||
Field::new("is_questionnair"),
|
||||
Field::new("parent").required(false).nullable(),
|
||||
Field::new("secure_group").required(false).nullable(),
|
||||
];
|
||||
static COUNTERPARTY_FIELDS: &[Field] = &[Field::new("id").read_only(), Field::new("name")];
|
||||
static CONTRACT_APPLICATION_FILE_FIELDS: &[Field] = &[
|
||||
Field::new("id").read_only(),
|
||||
Field::new("contract_id"),
|
||||
Field::new("contract_id").source("contract"),
|
||||
Field::new("name"),
|
||||
Field::new("file_type"),
|
||||
Field::new("file_type_display"),
|
||||
Field::new("file_type_display").source("get_file_type_display"),
|
||||
Field::new("status"),
|
||||
Field::new("status_display"),
|
||||
Field::new("absolute_url"),
|
||||
Field::new("scan_name"),
|
||||
Field::new("scan_url"),
|
||||
Field::new("local_path"),
|
||||
Field::new("status_display").source("get_status_display"),
|
||||
Field::new("absolute_url").source("get_absolute_url"),
|
||||
Field::new("scan_name").source("get_scan"),
|
||||
Field::new("scan_url").source("scan"),
|
||||
Field::new("local_path").default(empty_string),
|
||||
Field::new("comment"),
|
||||
Field::new("bill_total"),
|
||||
Field::new("bill_cost_total"),
|
||||
@@ -65,15 +88,25 @@ static CONTRACT_APPLICATION_FILE_FIELDS: &[Field] = &[
|
||||
];
|
||||
static COUNTERPARTY_RELATION: RelatedModel<Counterparty> =
|
||||
RelatedModel::new(counterparty_serializer);
|
||||
static CATEGORY_RELATION: RelatedModel<ContractCategory> =
|
||||
RelatedModel::new(contract_category_serializer);
|
||||
static CONTRACT_RELATION: RelatedModel<Contract> = RelatedModel::new(contractapp_serializer);
|
||||
static PROJECT_RELATION: RelatedModel<Project> = RelatedModel::new(project_serializer);
|
||||
static FRC_RELATION: RelatedModel<Frc> = RelatedModel::new(frc_serializer);
|
||||
static EMPLOYEE_RELATION: RelatedModel<Employee> = RelatedModel::new(employee_serializer);
|
||||
|
||||
fn empty_string() -> serde_json::Value {
|
||||
serde_json::Value::String(String::new())
|
||||
}
|
||||
|
||||
pub fn contractapp_serializer() -> ModelSerializer<Contract> {
|
||||
ModelSerializer::new(CONTRACT_FIELDS)
|
||||
}
|
||||
|
||||
pub fn contract_category_serializer() -> ModelSerializer<ContractCategory> {
|
||||
ModelSerializer::new(CONTRACT_CATEGORY_FIELDS)
|
||||
}
|
||||
|
||||
pub fn counterparty_serializer() -> ModelSerializer<Counterparty> {
|
||||
ModelSerializer::new(COUNTERPARTY_FIELDS)
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ use super::models::Frc;
|
||||
static FRC_FILTERS: &[Filter] = &[
|
||||
Filter::exact("id"),
|
||||
Filter::exact("name"),
|
||||
Filter::contains("name"),
|
||||
Filter::contains("name").local_only(),
|
||||
];
|
||||
|
||||
pub fn frc_filterset() -> FilterSet<Frc> {
|
||||
FilterSet::new(FRC_FILTERS)
|
||||
FilterSet::new(FRC_FILTERS).remote_ordering("order_by")
|
||||
}
|
||||
|
||||
@@ -16,8 +16,9 @@ impl AppModule for FrcModule {
|
||||
}
|
||||
|
||||
fn init(&self, ctx: &mut ModuleContext) {
|
||||
ctx.resource::<models::Frc>(
|
||||
ctx.cached_mapped_remote_resource::<models::Frc>(
|
||||
"frc",
|
||||
"/api/frc/frc/",
|
||||
serializers::frc_serializer(),
|
||||
filters::frc_filterset(),
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ static FRC_FIELDS: &[Field] = &[
|
||||
Field::new("id").read_only(),
|
||||
Field::new("name"),
|
||||
Field::new("icon"),
|
||||
Field::new("balance"),
|
||||
Field::new("balance").source("get_balance"),
|
||||
];
|
||||
|
||||
pub fn frc_serializer() -> ModelSerializer<Frc> {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
use che_tauri::{Filter, FilterSet};
|
||||
|
||||
use super::models::{Memo, MemoCategory};
|
||||
|
||||
static MEMO_FILTERS: &[Filter] = &[
|
||||
Filter::exact("id"),
|
||||
Filter::contains("text"),
|
||||
Filter::exact("date"),
|
||||
Filter::exact("priority"),
|
||||
Filter::exact("archive"),
|
||||
Filter::exact("cancel"),
|
||||
Filter::exact_source("recipient", "recipient_id").remote("recipient"),
|
||||
Filter::exact_source("sender", "sender_id").remote("sender"),
|
||||
Filter::exact_source("category", "category_id").remote("category"),
|
||||
Filter::exact_source("project", "project_id").remote("project"),
|
||||
];
|
||||
|
||||
static MEMO_CATEGORY_FILTERS: &[Filter] = &[
|
||||
Filter::exact("id"),
|
||||
Filter::exact("name"),
|
||||
Filter::contains("name"),
|
||||
Filter::exact("code"),
|
||||
];
|
||||
|
||||
pub fn memo_filterset() -> FilterSet<Memo> {
|
||||
FilterSet::new(MEMO_FILTERS).remote_ordering("order_by")
|
||||
}
|
||||
|
||||
pub fn memo_category_filterset() -> FilterSet<MemoCategory> {
|
||||
FilterSet::new(MEMO_CATEGORY_FILTERS).remote_ordering("order_by")
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
pub mod filters;
|
||||
pub mod models;
|
||||
pub mod serializers;
|
||||
|
||||
use che_tauri::{AppModule, ModuleContext};
|
||||
|
||||
pub fn module() -> InternalmemoappModule {
|
||||
InternalmemoappModule
|
||||
}
|
||||
|
||||
pub struct InternalmemoappModule;
|
||||
|
||||
impl AppModule for InternalmemoappModule {
|
||||
fn name(&self) -> &'static str {
|
||||
"internalmemoapp"
|
||||
}
|
||||
|
||||
fn init(&self, ctx: &mut ModuleContext) {
|
||||
ctx.mapped_remote_resource::<models::Memo>(
|
||||
"memo",
|
||||
"/api/internalmemo/memo/",
|
||||
serializers::memo_serializer(),
|
||||
filters::memo_filterset(),
|
||||
);
|
||||
ctx.mapped_remote_resource::<models::MemoCategory>(
|
||||
"memo_category",
|
||||
"/api/internalmemo/category/",
|
||||
serializers::memo_category_serializer(),
|
||||
filters::memo_category_filterset(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use che_orm::Model;
|
||||
|
||||
use crate::apps::{personemanagment::models::Employee, projectapp::models::Project};
|
||||
|
||||
#[derive(Debug, Clone, Model)]
|
||||
#[model(table = "internal_memo")]
|
||||
pub struct Memo {
|
||||
#[field(primary_key)]
|
||||
pub id: i64,
|
||||
|
||||
#[field(foreign_key = Employee)]
|
||||
pub recipient_id: Option<i64>,
|
||||
|
||||
#[field(foreign_key = Employee)]
|
||||
pub sender_id: Option<i64>,
|
||||
|
||||
#[field(foreign_key = MemoCategory)]
|
||||
pub category_id: Option<i64>,
|
||||
|
||||
#[field(foreign_key = Project)]
|
||||
pub project_id: Option<i64>,
|
||||
|
||||
pub task_set: Option<String>,
|
||||
pub priority_display: String,
|
||||
pub absolute_url: String,
|
||||
pub date: String,
|
||||
pub my_approve: String,
|
||||
pub text: String,
|
||||
pub resalution: String,
|
||||
pub priority: String,
|
||||
pub archive: bool,
|
||||
pub cancel: bool,
|
||||
pub date_start: Option<String>,
|
||||
pub date_end: Option<String>,
|
||||
pub value: f64,
|
||||
pub archive_s: bool,
|
||||
pub current_step: i64,
|
||||
pub subject: Option<String>,
|
||||
pub employee_acl: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Model)]
|
||||
#[model(table = "internal_memo_category")]
|
||||
pub struct MemoCategory {
|
||||
#[field(primary_key)]
|
||||
pub id: i64,
|
||||
|
||||
pub name: String,
|
||||
pub template: String,
|
||||
pub template_text: String,
|
||||
pub code: String,
|
||||
pub in_month_limit: bool,
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
use che_tauri::{Field, ModelSerializer, RelatedModel};
|
||||
|
||||
use crate::apps::{
|
||||
personemanagment::{models::Employee, serializers::employee_serializer},
|
||||
projectapp::{models::Project, serializers::project_serializer},
|
||||
};
|
||||
|
||||
use super::models::{Memo, MemoCategory};
|
||||
|
||||
static MEMO_FIELDS: &[Field] = &[
|
||||
Field::new("id").read_only(),
|
||||
Field::new("recipient_id")
|
||||
.source("recipient")
|
||||
.required(false)
|
||||
.nullable(),
|
||||
Field::new("sender_id")
|
||||
.source("sender")
|
||||
.required(false)
|
||||
.nullable(),
|
||||
Field::new("category_id")
|
||||
.source("category")
|
||||
.required(false)
|
||||
.nullable(),
|
||||
Field::new("project_id")
|
||||
.source("project")
|
||||
.required(false)
|
||||
.nullable(),
|
||||
Field::json("task_set")
|
||||
.ts_type("unknown[]")
|
||||
.required(false)
|
||||
.nullable(),
|
||||
Field::new("priority_display").source("get_priority_display"),
|
||||
Field::new("absolute_url").source("get_absolute_url"),
|
||||
Field::new("date"),
|
||||
Field::new("my_approve"),
|
||||
Field::new("text"),
|
||||
Field::new("resalution"),
|
||||
Field::new("priority"),
|
||||
Field::new("archive"),
|
||||
Field::new("cancel"),
|
||||
Field::new("date_start").required(false).nullable(),
|
||||
Field::new("date_end").required(false).nullable(),
|
||||
Field::new("value"),
|
||||
Field::new("archive_s"),
|
||||
Field::new("current_step"),
|
||||
Field::json("subject")
|
||||
.ts_type("number[]")
|
||||
.required(false)
|
||||
.nullable(),
|
||||
Field::json("employee_acl")
|
||||
.ts_type("number[]")
|
||||
.required(false)
|
||||
.nullable(),
|
||||
Field::related("recipient", "recipient_id", &EMPLOYEE_RELATION),
|
||||
Field::related("sender", "sender_id", &EMPLOYEE_RELATION),
|
||||
Field::related("category", "category_id", &MEMO_CATEGORY_RELATION),
|
||||
Field::related("project", "project_id", &PROJECT_RELATION),
|
||||
];
|
||||
|
||||
static MEMO_CATEGORY_FIELDS: &[Field] = &[
|
||||
Field::new("id").read_only(),
|
||||
Field::new("name"),
|
||||
Field::new("template"),
|
||||
Field::new("template_text"),
|
||||
Field::new("code"),
|
||||
Field::new("in_month_limit"),
|
||||
];
|
||||
|
||||
static EMPLOYEE_RELATION: RelatedModel<Employee> = RelatedModel::new(employee_serializer);
|
||||
static PROJECT_RELATION: RelatedModel<Project> = RelatedModel::new(project_serializer);
|
||||
static MEMO_CATEGORY_RELATION: RelatedModel<MemoCategory> =
|
||||
RelatedModel::new(memo_category_serializer);
|
||||
|
||||
pub fn memo_serializer() -> ModelSerializer<Memo> {
|
||||
ModelSerializer::new(MEMO_FIELDS)
|
||||
}
|
||||
|
||||
pub fn memo_category_serializer() -> ModelSerializer<MemoCategory> {
|
||||
ModelSerializer::new(MEMO_CATEGORY_FIELDS)
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
pub mod frcapp;
|
||||
pub mod internalmemoapp;
|
||||
pub mod personemanagment;
|
||||
pub mod projectapp;
|
||||
pub mod supplyapp;
|
||||
pub mod users;
|
||||
|
||||
use che_tauri::InstalledApps;
|
||||
@@ -12,5 +14,7 @@ pub fn installed_apps() -> InstalledApps {
|
||||
.add(projectapp::module())
|
||||
.add(personemanagment::module())
|
||||
.add(contractapp::module())
|
||||
.add(supplyapp::module())
|
||||
.add(internalmemoapp::module())
|
||||
}
|
||||
pub mod contractapp;
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
use che_tauri::{Filter, FilterSet};
|
||||
|
||||
use super::models::{Employee, Message, Task};
|
||||
use super::models::{Employee, Message, Task, TaskTransfer};
|
||||
|
||||
static EMPLOYEE_FILTERS: &[Filter] = &[
|
||||
Filter::exact("id"),
|
||||
Filter::exact("name"),
|
||||
Filter::contains("name"),
|
||||
Filter::contains("name").remote("name"),
|
||||
Filter::exact("short_name"),
|
||||
Filter::contains("short_name"),
|
||||
Filter::contains("short_name").remote("name"),
|
||||
];
|
||||
|
||||
static TASK_FILTERS: &[Filter] = &[
|
||||
Filter::exact("id"),
|
||||
Filter::exact("name"),
|
||||
Filter::contains("name"),
|
||||
Filter::exact("author_id"),
|
||||
Filter::exact("responsible_id"),
|
||||
Filter::contains("text"),
|
||||
Filter::remote_only("contract"),
|
||||
Filter::remote_only("bill"),
|
||||
Filter::remote_only("memo"),
|
||||
Filter::remote_only("doer"),
|
||||
Filter::remote_only("author"),
|
||||
Filter::exact("archive"),
|
||||
Filter::exact("typ"),
|
||||
Filter::exact("status"),
|
||||
Filter::remote_only("q"),
|
||||
Filter::remote_only("incomplete"),
|
||||
];
|
||||
|
||||
static MESSAGE_FILTERS: &[Filter] = &[
|
||||
@@ -24,14 +31,26 @@ static MESSAGE_FILTERS: &[Filter] = &[
|
||||
Filter::exact("employee_id"),
|
||||
];
|
||||
|
||||
static TASK_TRANSFER_FILTERS: &[Filter] = &[
|
||||
Filter::exact("id"),
|
||||
Filter::remote_only("employee_from"),
|
||||
Filter::remote_only("employee_to"),
|
||||
Filter::exact("status"),
|
||||
Filter::exact("typ"),
|
||||
];
|
||||
|
||||
pub fn employee_filterset() -> FilterSet<Employee> {
|
||||
FilterSet::new(EMPLOYEE_FILTERS)
|
||||
FilterSet::new(EMPLOYEE_FILTERS).remote_ordering("order_by")
|
||||
}
|
||||
|
||||
pub fn task_filterset() -> FilterSet<Task> {
|
||||
FilterSet::new(TASK_FILTERS)
|
||||
FilterSet::new(TASK_FILTERS).remote_ordering("order_by")
|
||||
}
|
||||
|
||||
pub fn message_filterset() -> FilterSet<Message> {
|
||||
FilterSet::new(MESSAGE_FILTERS)
|
||||
}
|
||||
|
||||
pub fn task_transfer_filterset() -> FilterSet<TaskTransfer> {
|
||||
FilterSet::new(TASK_TRANSFER_FILTERS).remote_ordering("order_by")
|
||||
}
|
||||
|
||||
@@ -16,18 +16,27 @@ impl AppModule for TaskModule {
|
||||
}
|
||||
|
||||
fn init(&self, ctx: &mut ModuleContext) {
|
||||
ctx.resource::<models::Employee>(
|
||||
ctx.cached_mapped_remote_resource::<models::Employee>(
|
||||
"employee",
|
||||
"/api/persone/employee/",
|
||||
serializers::employee_serializer(),
|
||||
filters::employee_filterset(),
|
||||
);
|
||||
ctx.resource::<models::Task>(
|
||||
ctx.mapped_remote_resource::<models::Task>(
|
||||
"task",
|
||||
"/api/persone/task/",
|
||||
serializers::task_serializer(),
|
||||
filters::task_filterset(),
|
||||
);
|
||||
ctx.resource::<models::Message>(
|
||||
ctx.mapped_remote_resource::<models::TaskTransfer>(
|
||||
"task_transfer",
|
||||
"/api/persone/task_transfer/",
|
||||
serializers::task_transfer_serializer(),
|
||||
filters::task_transfer_filterset(),
|
||||
);
|
||||
ctx.mapped_remote_resource::<models::Message>(
|
||||
"message",
|
||||
"/api/persone/messages/",
|
||||
serializers::message_serializer(),
|
||||
filters::message_filterset(),
|
||||
);
|
||||
|
||||
@@ -19,13 +19,73 @@ pub struct Task {
|
||||
#[field(primary_key)]
|
||||
pub id: i64,
|
||||
|
||||
pub name: String,
|
||||
|
||||
#[field(foreign_key = Employee)]
|
||||
pub author_id: Option<i64>,
|
||||
|
||||
#[field(foreign_key = Employee)]
|
||||
pub responsible_id: Option<i64>,
|
||||
pub project: Option<String>,
|
||||
pub doer: Option<String>,
|
||||
pub doer_name: String,
|
||||
pub author: Option<String>,
|
||||
pub memo_full: Option<String>,
|
||||
pub bill_full: Option<String>,
|
||||
pub contract_full: Option<String>,
|
||||
pub contract_application_full: Option<String>,
|
||||
pub outgoing_letter_full: Option<String>,
|
||||
pub entry_letter_full: Option<String>,
|
||||
pub protocolitem_full: Option<String>,
|
||||
pub decree_full: Option<String>,
|
||||
pub delivery_full: Option<String>,
|
||||
pub get_status: String,
|
||||
pub get_status_class: String,
|
||||
pub get_scan_url: Option<String>,
|
||||
pub get_last_day: String,
|
||||
pub frc_icon: String,
|
||||
pub uploadfile_set: Option<String>,
|
||||
pub deadline: String,
|
||||
pub request_new_deadline: Option<String>,
|
||||
pub plan_date: Option<String>,
|
||||
pub date: String,
|
||||
pub message_set: Option<String>,
|
||||
pub responsible: Option<String>,
|
||||
pub counterparty: Option<String>,
|
||||
pub get_deadline_history: Option<String>,
|
||||
pub duration: Option<i64>,
|
||||
pub bid_full: Option<String>,
|
||||
pub price_agreement_full: Option<String>,
|
||||
pub transfer: Option<String>,
|
||||
pub text: String,
|
||||
pub status: String,
|
||||
pub result: String,
|
||||
pub complit_date: Option<String>,
|
||||
pub comment: String,
|
||||
pub note: String,
|
||||
pub approve: bool,
|
||||
pub archive: bool,
|
||||
pub approve_date: Option<String>,
|
||||
pub approve_required: bool,
|
||||
pub progress_status: String,
|
||||
pub start_date: Option<String>,
|
||||
pub end_date: Option<String>,
|
||||
pub order_number: Option<i64>,
|
||||
pub priority: Option<i64>,
|
||||
pub typ: String,
|
||||
pub stage: Option<String>,
|
||||
pub contract: Option<String>,
|
||||
pub questionnair: Option<String>,
|
||||
pub contract_application: Option<String>,
|
||||
pub entry_letter: Option<String>,
|
||||
pub outgoing_letter: Option<String>,
|
||||
pub protocol: Option<String>,
|
||||
pub bill: Option<String>,
|
||||
pub decree: Option<String>,
|
||||
pub court_case: Option<String>,
|
||||
pub bill_register: Option<String>,
|
||||
pub price_agreement: Option<String>,
|
||||
pub bid: Option<String>,
|
||||
pub delivery: Option<String>,
|
||||
pub scheduled_task: Option<String>,
|
||||
pub report: Option<String>,
|
||||
pub memo: Option<String>,
|
||||
pub protocolitem: Option<String>,
|
||||
pub related_note: Option<String>,
|
||||
pub task: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Model)]
|
||||
@@ -41,4 +101,25 @@ pub struct Message {
|
||||
pub employee_id: i64,
|
||||
|
||||
pub text: String,
|
||||
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Model)]
|
||||
#[model(table = "task_transfer")]
|
||||
pub struct TaskTransfer {
|
||||
#[field(primary_key)]
|
||||
pub id: i64,
|
||||
|
||||
pub employee_from: Option<String>,
|
||||
|
||||
pub employee_to: Option<String>,
|
||||
|
||||
pub date_create: String,
|
||||
|
||||
pub task: Option<String>,
|
||||
|
||||
pub status: String,
|
||||
|
||||
pub typ: String,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use che_tauri::{Field, ModelSerializer};
|
||||
|
||||
use super::models::{Employee, Message, Task};
|
||||
use super::models::{Employee, Message, Task, TaskTransfer};
|
||||
|
||||
static EMPLOYEE_FIELDS: &[Field] = &[
|
||||
Field::new("id").read_only(),
|
||||
@@ -11,16 +11,281 @@ static EMPLOYEE_FIELDS: &[Field] = &[
|
||||
|
||||
static TASK_FIELDS: &[Field] = &[
|
||||
Field::new("id").read_only(),
|
||||
Field::new("name"),
|
||||
Field::new("author_id").required(false).nullable(),
|
||||
Field::new("responsible_id").required(false).nullable(),
|
||||
Field::json("project")
|
||||
.ts_type("string")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("doer")
|
||||
.ts_type("Employee")
|
||||
.input_ts_type("Employee")
|
||||
.required(false)
|
||||
.nullable(),
|
||||
Field::new("doer_name").read_only(),
|
||||
Field::json("author")
|
||||
.ts_type("Employee")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("memo_full")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("bill_full")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("contract_full")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("contract_application_full")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("outgoing_letter_full")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("entry_letter_full")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("protocolitem_full")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("decree_full")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("delivery_full")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::new("get_status").read_only(),
|
||||
Field::new("get_status_class").read_only(),
|
||||
Field::new("get_scan_url")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::new("get_last_day").read_only(),
|
||||
Field::new("frc_icon").read_only(),
|
||||
Field::json("uploadfile_set")
|
||||
.ts_type("unknown[]")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::new("deadline"),
|
||||
Field::new("request_new_deadline")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::new("plan_date")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::new("date").read_only(),
|
||||
Field::json("message_set")
|
||||
.ts_type("unknown[]")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("responsible")
|
||||
.ts_type("Employee")
|
||||
.input_ts_type("Employee")
|
||||
.required(false)
|
||||
.nullable(),
|
||||
Field::json("counterparty")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("get_deadline_history")
|
||||
.ts_type("unknown[]")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::new("duration")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("bid_full")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("price_agreement_full")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("transfer")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::new("text"),
|
||||
Field::new("status").read_only(),
|
||||
Field::new("result").read_only(),
|
||||
Field::new("complit_date")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::new("comment").read_only(),
|
||||
Field::new("note").read_only(),
|
||||
Field::new("approve").read_only(),
|
||||
Field::new("archive").read_only(),
|
||||
Field::new("approve_date")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::new("approve_required").read_only(),
|
||||
Field::new("progress_status").read_only(),
|
||||
Field::new("start_date")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::new("end_date")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::new("order_number")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::new("priority")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::new("typ").read_only(),
|
||||
Field::json("stage")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("contract")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("questionnair")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("contract_application")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("entry_letter")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("outgoing_letter")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("protocol")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("bill")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("decree")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("court_case")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("bill_register")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("price_agreement")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("bid")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("delivery")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("scheduled_task")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("report")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("memo")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("protocolitem")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("related_note")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
Field::json("task")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable()
|
||||
.read_only(),
|
||||
];
|
||||
|
||||
static MESSAGE_FIELDS: &[Field] = &[
|
||||
Field::new("id").read_only(),
|
||||
Field::new("task_id"),
|
||||
Field::new("employee_id"),
|
||||
Field::new("task").source("task_id"),
|
||||
Field::new("recipient").source("employee_id"),
|
||||
Field::new("text"),
|
||||
Field::new("status"),
|
||||
];
|
||||
|
||||
static TASK_TRANSFER_FIELDS: &[Field] = &[
|
||||
Field::new("id").read_only(),
|
||||
Field::json("employee_from").required(false).nullable(),
|
||||
Field::json("employee_to").required(false).nullable(),
|
||||
Field::new("date_create").read_only(),
|
||||
Field::json("task").required(false).nullable(),
|
||||
Field::new("status"),
|
||||
Field::new("typ"),
|
||||
];
|
||||
|
||||
pub fn employee_serializer() -> ModelSerializer<Employee> {
|
||||
@@ -34,3 +299,7 @@ pub fn task_serializer() -> ModelSerializer<Task> {
|
||||
pub fn message_serializer() -> ModelSerializer<Message> {
|
||||
ModelSerializer::new(MESSAGE_FIELDS)
|
||||
}
|
||||
|
||||
pub fn task_transfer_serializer() -> ModelSerializer<TaskTransfer> {
|
||||
ModelSerializer::new(TASK_TRANSFER_FIELDS)
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ use super::models::Project;
|
||||
static PROJECT_FILTERS: &[Filter] = &[
|
||||
Filter::exact("id"),
|
||||
Filter::exact("name"),
|
||||
Filter::contains("name"),
|
||||
Filter::contains("name").remote("search"),
|
||||
Filter::exact("short_name"),
|
||||
Filter::contains("short_name"),
|
||||
Filter::contains("short_name").remote("search"),
|
||||
];
|
||||
|
||||
pub fn project_filterset() -> FilterSet<Project> {
|
||||
FilterSet::new(PROJECT_FILTERS)
|
||||
FilterSet::new(PROJECT_FILTERS).remote_ordering("order_by")
|
||||
}
|
||||
|
||||
@@ -16,8 +16,9 @@ impl AppModule for ProjectModule {
|
||||
}
|
||||
|
||||
fn init(&self, ctx: &mut ModuleContext) {
|
||||
ctx.resource::<models::Project>(
|
||||
ctx.cached_mapped_remote_resource::<models::Project>(
|
||||
"project",
|
||||
"/api/project/",
|
||||
serializers::project_serializer(),
|
||||
filters::project_filterset(),
|
||||
);
|
||||
|
||||
@@ -6,9 +6,15 @@ static PROJECT_FIELDS: &[Field] = &[
|
||||
Field::new("id").read_only(),
|
||||
Field::new("name"),
|
||||
Field::new("full_name"),
|
||||
Field::new("short_name"),
|
||||
Field::new("locality_id").required(false).nullable(),
|
||||
Field::new("locality_name").required(false).nullable(),
|
||||
Field::new("short_name").source("get_short_name"),
|
||||
Field::new("locality_id")
|
||||
.source("locality")
|
||||
.required(false)
|
||||
.nullable(),
|
||||
Field::new("locality_name")
|
||||
.source("locality.name")
|
||||
.required(false)
|
||||
.nullable(),
|
||||
];
|
||||
|
||||
pub fn project_serializer() -> ModelSerializer<Project> {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
use che_tauri::{Filter, FilterSet};
|
||||
|
||||
use super::models::Bill;
|
||||
|
||||
static BILL_FILTERS: &[Filter] = &[
|
||||
Filter::exact("id"),
|
||||
Filter::exact("number"),
|
||||
Filter::contains("number"),
|
||||
Filter::exact("text"),
|
||||
Filter::contains("text"),
|
||||
Filter::exact("status"),
|
||||
Filter::exact("status_name"),
|
||||
Filter::contains("status_name"),
|
||||
Filter::exact("date"),
|
||||
Filter::exact("date_due"),
|
||||
Filter::exact("date_bill"),
|
||||
Filter::exact("contract_typ"),
|
||||
Filter::remote_only("counterparty"),
|
||||
Filter::remote_only("frc"),
|
||||
Filter::remote_only("project"),
|
||||
Filter::remote_only("category"),
|
||||
];
|
||||
|
||||
pub fn bill_filterset() -> FilterSet<Bill> {
|
||||
FilterSet::new(BILL_FILTERS).remote_ordering("order_by")
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
CREATE TABLE IF NOT EXISTS supply_bill (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
number TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
status_name TEXT NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
date_due TEXT,
|
||||
date_bill TEXT,
|
||||
month_of_costs TEXT,
|
||||
cost REAL NOT NULL,
|
||||
to_payd REAL NOT NULL,
|
||||
paid REAL NOT NULL,
|
||||
nds_cost REAL NOT NULL,
|
||||
scan TEXT,
|
||||
comment TEXT NOT NULL,
|
||||
absolute_url TEXT NOT NULL,
|
||||
date_pay TEXT,
|
||||
transaction_date TEXT,
|
||||
date_applay TEXT,
|
||||
archive_s BOOLEAN NOT NULL,
|
||||
pp_maked BOOLEAN NOT NULL,
|
||||
composit BOOLEAN NOT NULL,
|
||||
contract_typ TEXT NOT NULL,
|
||||
frc TEXT,
|
||||
project TEXT,
|
||||
counterparty TEXT,
|
||||
contract TEXT,
|
||||
responsible TEXT,
|
||||
author TEXT,
|
||||
category TEXT,
|
||||
transferdocument_set TEXT
|
||||
);
|
||||
@@ -0,0 +1,350 @@
|
||||
{
|
||||
"models": [
|
||||
{
|
||||
"table": "supply_bill",
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"ty": "integer",
|
||||
"primary_key": true,
|
||||
"nullable": false,
|
||||
"auto": true,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "number",
|
||||
"ty": "text",
|
||||
"primary_key": false,
|
||||
"nullable": false,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "text",
|
||||
"ty": "text",
|
||||
"primary_key": false,
|
||||
"nullable": false,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"ty": "text",
|
||||
"primary_key": false,
|
||||
"nullable": false,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "status_name",
|
||||
"ty": "text",
|
||||
"primary_key": false,
|
||||
"nullable": false,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "date",
|
||||
"ty": "text",
|
||||
"primary_key": false,
|
||||
"nullable": false,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "date_due",
|
||||
"ty": "text",
|
||||
"primary_key": false,
|
||||
"nullable": true,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "date_bill",
|
||||
"ty": "text",
|
||||
"primary_key": false,
|
||||
"nullable": true,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "month_of_costs",
|
||||
"ty": "text",
|
||||
"primary_key": false,
|
||||
"nullable": true,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "cost",
|
||||
"ty": "real",
|
||||
"primary_key": false,
|
||||
"nullable": false,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "to_payd",
|
||||
"ty": "real",
|
||||
"primary_key": false,
|
||||
"nullable": false,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "paid",
|
||||
"ty": "real",
|
||||
"primary_key": false,
|
||||
"nullable": false,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "nds_cost",
|
||||
"ty": "real",
|
||||
"primary_key": false,
|
||||
"nullable": false,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "scan",
|
||||
"ty": "text",
|
||||
"primary_key": false,
|
||||
"nullable": true,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "comment",
|
||||
"ty": "text",
|
||||
"primary_key": false,
|
||||
"nullable": false,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "absolute_url",
|
||||
"ty": "text",
|
||||
"primary_key": false,
|
||||
"nullable": false,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "date_pay",
|
||||
"ty": "text",
|
||||
"primary_key": false,
|
||||
"nullable": true,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "transaction_date",
|
||||
"ty": "text",
|
||||
"primary_key": false,
|
||||
"nullable": true,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "date_applay",
|
||||
"ty": "text",
|
||||
"primary_key": false,
|
||||
"nullable": true,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "archive_s",
|
||||
"ty": "boolean",
|
||||
"primary_key": false,
|
||||
"nullable": false,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "pp_maked",
|
||||
"ty": "boolean",
|
||||
"primary_key": false,
|
||||
"nullable": false,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "composit",
|
||||
"ty": "boolean",
|
||||
"primary_key": false,
|
||||
"nullable": false,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "contract_typ",
|
||||
"ty": "text",
|
||||
"primary_key": false,
|
||||
"nullable": false,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "frc",
|
||||
"ty": "text",
|
||||
"primary_key": false,
|
||||
"nullable": true,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "project",
|
||||
"ty": "text",
|
||||
"primary_key": false,
|
||||
"nullable": true,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "counterparty",
|
||||
"ty": "text",
|
||||
"primary_key": false,
|
||||
"nullable": true,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "contract",
|
||||
"ty": "text",
|
||||
"primary_key": false,
|
||||
"nullable": true,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "responsible",
|
||||
"ty": "text",
|
||||
"primary_key": false,
|
||||
"nullable": true,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "author",
|
||||
"ty": "text",
|
||||
"primary_key": false,
|
||||
"nullable": true,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "category",
|
||||
"ty": "text",
|
||||
"primary_key": false,
|
||||
"nullable": true,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
},
|
||||
{
|
||||
"name": "transferdocument_set",
|
||||
"ty": "text",
|
||||
"primary_key": false,
|
||||
"nullable": true,
|
||||
"auto": false,
|
||||
"unique": false,
|
||||
"max_length": null,
|
||||
"default": null,
|
||||
"foreign_key": null
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
pub mod filters;
|
||||
pub mod models;
|
||||
pub mod serializers;
|
||||
|
||||
use che_tauri::{AppModule, ModuleContext};
|
||||
|
||||
pub fn module() -> SupplyappModule {
|
||||
SupplyappModule
|
||||
}
|
||||
|
||||
pub struct SupplyappModule;
|
||||
|
||||
impl AppModule for SupplyappModule {
|
||||
fn name(&self) -> &'static str {
|
||||
"supplyapp"
|
||||
}
|
||||
|
||||
fn init(&self, ctx: &mut ModuleContext) {
|
||||
ctx.mapped_remote_resource::<models::Bill>(
|
||||
"bill",
|
||||
"/api/supply/bill/",
|
||||
serializers::bill_serializer(),
|
||||
filters::bill_filterset(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use che_orm::Model;
|
||||
|
||||
#[derive(Debug, Clone, Model)]
|
||||
#[model(table = "supply_bill")]
|
||||
pub struct Bill {
|
||||
#[field(primary_key)]
|
||||
pub id: i64,
|
||||
|
||||
pub number: String,
|
||||
pub text: String,
|
||||
pub status: String,
|
||||
pub status_name: String,
|
||||
pub date: String,
|
||||
pub date_due: Option<String>,
|
||||
pub date_bill: Option<String>,
|
||||
pub month_of_costs: Option<String>,
|
||||
pub cost: f64,
|
||||
pub to_payd: f64,
|
||||
pub paid: f64,
|
||||
pub nds_cost: f64,
|
||||
pub scan: Option<String>,
|
||||
pub comment: String,
|
||||
pub absolute_url: String,
|
||||
pub date_pay: Option<String>,
|
||||
pub transaction_date: Option<String>,
|
||||
pub date_applay: Option<String>,
|
||||
pub archive_s: bool,
|
||||
pub pp_maked: bool,
|
||||
pub composit: bool,
|
||||
pub contract_typ: String,
|
||||
|
||||
pub frc: Option<String>,
|
||||
pub project: Option<String>,
|
||||
pub counterparty: Option<String>,
|
||||
pub contract: Option<String>,
|
||||
pub responsible: Option<String>,
|
||||
pub author: Option<String>,
|
||||
pub category: Option<String>,
|
||||
pub transferdocument_set: Option<String>,
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
use che_tauri::{Field, ModelSerializer};
|
||||
|
||||
use super::models::Bill;
|
||||
|
||||
static BILL_FIELDS: &[Field] = &[
|
||||
Field::new("id").read_only(),
|
||||
Field::new("number"),
|
||||
Field::new("text"),
|
||||
Field::new("status"),
|
||||
Field::new("status_name").source("get_status_display"),
|
||||
Field::new("date"),
|
||||
Field::new("date_due").required(false).nullable(),
|
||||
Field::new("date_bill").required(false).nullable(),
|
||||
Field::new("month_of_costs").required(false).nullable(),
|
||||
Field::new("cost"),
|
||||
Field::new("to_payd"),
|
||||
Field::new("paid"),
|
||||
Field::new("nds_cost"),
|
||||
Field::new("scan").required(false).nullable(),
|
||||
Field::new("comment"),
|
||||
Field::new("absolute_url").source("get_absolute_url"),
|
||||
Field::new("date_pay").required(false).nullable(),
|
||||
Field::new("transaction_date").required(false).nullable(),
|
||||
Field::new("date_applay").required(false).nullable(),
|
||||
Field::new("archive_s"),
|
||||
Field::new("pp_maked"),
|
||||
Field::new("composit"),
|
||||
Field::new("contract_typ"),
|
||||
Field::json("frc")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable(),
|
||||
Field::json("project")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable(),
|
||||
Field::json("counterparty")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable(),
|
||||
Field::json("contract")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable(),
|
||||
Field::json("responsible")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable(),
|
||||
Field::json("author")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable(),
|
||||
Field::json("category")
|
||||
.ts_type("unknown")
|
||||
.required(false)
|
||||
.nullable(),
|
||||
Field::json("transferdocument_set")
|
||||
.ts_type("unknown[]")
|
||||
.required(false)
|
||||
.nullable(),
|
||||
];
|
||||
|
||||
pub fn bill_serializer() -> ModelSerializer<Bill> {
|
||||
ModelSerializer::new(BILL_FIELDS)
|
||||
}
|
||||
@@ -1,11 +1,151 @@
|
||||
pub mod apps;
|
||||
pub mod sync;
|
||||
|
||||
use che_tauri::{ApiError, ApiRequest, AppState, AuthTokenResponse, TauriApi};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use che_orm::SqliteBackend;
|
||||
use che_tauri::{
|
||||
ApiError, ApiRequest, AppConfig, AppState, AuthTokenResponse, DatabaseConfig, RemoteConfig,
|
||||
TauriApi,
|
||||
};
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
|
||||
use crate::sync::SyncContractsResult;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "system" fn Java_com_che_ewa_1mobile_MainActivity_initRustlsPlatformVerifier(
|
||||
mut env: jni::JNIEnv,
|
||||
_class: jni::objects::JClass,
|
||||
context: jni::objects::JObject,
|
||||
) {
|
||||
if let Err(error) = rustls_platform_verifier::android::init_with_env(&mut env, context) {
|
||||
eprintln!("failed to initialize rustls platform verifier: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
const REMOTE_BASE_URL: &str = "http://10.0.2.2:8000";
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
const REMOTE_BASE_URL: &str = "http://127.0.0.1:8000";
|
||||
|
||||
const DEFAULT_AUTH_PATH: &str = "/api-token-auth/";
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||
struct AppSettings {
|
||||
remote_base_url: String,
|
||||
auth_path: String,
|
||||
}
|
||||
|
||||
impl Default for AppSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
remote_base_url: String::from(REMOTE_BASE_URL),
|
||||
auth_path: String::from(DEFAULT_AUTH_PATH),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_api(database_url: String, settings: AppSettings) -> Result<TauriApi, ApiError> {
|
||||
let config = AppConfig {
|
||||
database: DatabaseConfig { url: database_url },
|
||||
remote: Some(settings_to_remote_config(&settings)),
|
||||
};
|
||||
let db = SqliteBackend::connect(&config.database.url).await?;
|
||||
let state = AppState::new(config, db);
|
||||
|
||||
TauriApi::new(state)
|
||||
.install(apps::installed_apps())
|
||||
.build()
|
||||
.await
|
||||
}
|
||||
|
||||
fn settings_to_remote_config(settings: &AppSettings) -> RemoteConfig {
|
||||
RemoteConfig {
|
||||
base_url: normalize_base_url(&settings.remote_base_url),
|
||||
auth_path: Some(normalize_auth_path(&settings.auth_path)),
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_settings(settings: AppSettings) -> AppSettings {
|
||||
AppSettings {
|
||||
remote_base_url: normalize_base_url(&settings.remote_base_url),
|
||||
auth_path: normalize_auth_path(&settings.auth_path),
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_base_url(base_url: &str) -> String {
|
||||
base_url.trim().trim_end_matches('/').to_string()
|
||||
}
|
||||
|
||||
fn normalize_auth_path(auth_path: &str) -> String {
|
||||
let clean = auth_path.trim().trim_matches('/');
|
||||
if clean.is_empty() {
|
||||
String::from(DEFAULT_AUTH_PATH)
|
||||
} else {
|
||||
format!("/{clean}/")
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_settings(settings: &AppSettings) -> Result<(), ApiError> {
|
||||
if !(settings.remote_base_url.starts_with("http://")
|
||||
|| settings.remote_base_url.starts_with("https://"))
|
||||
{
|
||||
return Err(ApiError::bad_request(
|
||||
"Удаленный URL должен начинаться с http:// или https://",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn remote_http_client() -> reqwest::Client {
|
||||
reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(15))
|
||||
.connect_timeout(Duration::from_secs(5))
|
||||
.build()
|
||||
.expect("failed to build reqwest client")
|
||||
}
|
||||
|
||||
fn settings_path(app_data_dir: &Path) -> PathBuf {
|
||||
app_data_dir.join("settings.json")
|
||||
}
|
||||
|
||||
async fn load_settings(app_data_dir: &Path) -> Result<AppSettings, ApiError> {
|
||||
let path = settings_path(app_data_dir);
|
||||
match tokio::fs::read_to_string(path).await {
|
||||
Ok(content) => serde_json::from_str::<AppSettings>(&content)
|
||||
.map(normalize_settings)
|
||||
.map_err(|error| ApiError::new("settings_error", error.to_string())),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(AppSettings::default()),
|
||||
Err(error) => Err(ApiError::new("settings_error", error.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
async fn save_settings(app_data_dir: &Path, settings: &AppSettings) -> Result<(), ApiError> {
|
||||
tokio::fs::create_dir_all(app_data_dir)
|
||||
.await
|
||||
.map_err(|error| ApiError::new("settings_error", error.to_string()))?;
|
||||
let content = serde_json::to_string_pretty(settings)
|
||||
.map_err(|error| ApiError::new("settings_error", error.to_string()))?;
|
||||
tokio::fs::write(settings_path(app_data_dir), content)
|
||||
.await
|
||||
.map_err(|error| ApiError::new("settings_error", error.to_string()))
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
struct CurrentEmployee {
|
||||
id: i64,
|
||||
name: String,
|
||||
short_name: String,
|
||||
avatar: Option<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn greet(name: &str) -> String {
|
||||
format!("Hello, {}! You've been greeted from Rust!", name)
|
||||
@@ -43,6 +183,90 @@ fn auth_status(api: tauri::State<'_, TauriApi>) -> bool {
|
||||
api.is_authenticated()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_app_settings(app: tauri::AppHandle) -> Result<AppSettings, ApiError> {
|
||||
let app_data_dir = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|error| ApiError::new("settings_error", error.to_string()))?;
|
||||
load_settings(&app_data_dir).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn update_app_settings(
|
||||
app: tauri::AppHandle,
|
||||
api: tauri::State<'_, TauriApi>,
|
||||
settings: AppSettings,
|
||||
) -> Result<AppSettings, ApiError> {
|
||||
let settings = normalize_settings(settings);
|
||||
validate_settings(&settings)?;
|
||||
|
||||
let app_data_dir = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|error| ApiError::new("settings_error", error.to_string()))?;
|
||||
save_settings(&app_data_dir, &settings).await?;
|
||||
api.state()
|
||||
.set_remote_config(Some(settings_to_remote_config(&settings)));
|
||||
api.logout();
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn reset_app_settings(
|
||||
app: tauri::AppHandle,
|
||||
api: tauri::State<'_, TauriApi>,
|
||||
) -> Result<AppSettings, ApiError> {
|
||||
let settings = AppSettings::default();
|
||||
let app_data_dir = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|error| ApiError::new("settings_error", error.to_string()))?;
|
||||
save_settings(&app_data_dir, &settings).await?;
|
||||
api.state()
|
||||
.set_remote_config(Some(settings_to_remote_config(&settings)));
|
||||
api.logout();
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn current_employee(api: tauri::State<'_, TauriApi>) -> Result<CurrentEmployee, ApiError> {
|
||||
let remote = api.state().remote_config().ok_or_else(|| {
|
||||
ApiError::bad_request("current_employee requires [remote].base_url config")
|
||||
})?;
|
||||
|
||||
let token = api
|
||||
.state()
|
||||
.auth_token()
|
||||
.ok_or_else(|| ApiError::new("not_authenticated", "authentication token is missing"))?;
|
||||
|
||||
let response = remote_http_client()
|
||||
.get(format!(
|
||||
"{}/api/persone/employee/who_im/",
|
||||
remote.base_url.trim_end_matches('/')
|
||||
))
|
||||
.header(reqwest::header::AUTHORIZATION, format!("Token {token}"))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| ApiError::new("remote_error", error.to_string()))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let detail = response.text().await.unwrap_or_default();
|
||||
return Err(ApiError::new(
|
||||
"remote_error",
|
||||
format!("who_im request failed with {status}: {detail}"),
|
||||
));
|
||||
}
|
||||
|
||||
response
|
||||
.json::<CurrentEmployee>()
|
||||
.await
|
||||
.map_err(|error| ApiError::new("remote_error", error.to_string()))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn sync_contracts(
|
||||
app: tauri::AppHandle,
|
||||
@@ -55,6 +279,146 @@ async fn sync_contracts(
|
||||
sync::sync_contracts(&api, app_data_dir).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn load_remote_file(
|
||||
app: tauri::AppHandle,
|
||||
api: tauri::State<'_, TauriApi>,
|
||||
url: String,
|
||||
) -> Result<Vec<u8>, ApiError> {
|
||||
let path = cache_remote_file(&app, &api, &url).await?;
|
||||
tokio::fs::read(path)
|
||||
.await
|
||||
.map_err(|error| ApiError::new("file_error", error.to_string()))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn ensure_remote_file(
|
||||
app: tauri::AppHandle,
|
||||
api: tauri::State<'_, TauriApi>,
|
||||
url: String,
|
||||
) -> Result<String, ApiError> {
|
||||
let path = cache_remote_file(&app, &api, &url).await?;
|
||||
Ok(path.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn open_remote_file(
|
||||
app: tauri::AppHandle,
|
||||
api: tauri::State<'_, TauriApi>,
|
||||
url: String,
|
||||
) -> Result<(), ApiError> {
|
||||
let path = cache_remote_file(&app, &api, &url).await?;
|
||||
app.opener()
|
||||
.open_path(path.to_string_lossy().into_owned(), None::<&str>)
|
||||
.map_err(|error| ApiError::new("file_error", error.to_string()))
|
||||
}
|
||||
|
||||
async fn cache_remote_file(
|
||||
app: &tauri::AppHandle,
|
||||
api: &tauri::State<'_, TauriApi>,
|
||||
url: &str,
|
||||
) -> Result<PathBuf, ApiError> {
|
||||
if url.trim().is_empty() {
|
||||
return Err(ApiError::new("file_error", "Файл недоступен"));
|
||||
}
|
||||
|
||||
let remote = api.state().remote_config().ok_or_else(|| {
|
||||
ApiError::bad_request("load_remote_file requires [remote].base_url config")
|
||||
})?;
|
||||
let file_url = normalize_remote_file_url(&remote.base_url, url)?;
|
||||
let relative_path = remote_file_cache_path(file_url.path())?;
|
||||
let app_data_dir = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|error| ApiError::new("file_error", error.to_string()))?;
|
||||
let path = app_data_dir.join("remote_files").join(relative_path);
|
||||
|
||||
match tokio::fs::metadata(&path).await {
|
||||
Ok(metadata) if metadata.len() > 0 => {
|
||||
return Ok(path);
|
||||
}
|
||||
Ok(_) | Err(_) => {}
|
||||
}
|
||||
|
||||
let token = api
|
||||
.state()
|
||||
.auth_token()
|
||||
.ok_or_else(|| ApiError::new("not_authenticated", "authentication token is missing"))?;
|
||||
|
||||
let response = remote_http_client()
|
||||
.get(file_url)
|
||||
.header(reqwest::header::AUTHORIZATION, format!("Token {token}"))
|
||||
.send()
|
||||
.await?;
|
||||
if !response.status().is_success() {
|
||||
return Err(ApiError::new(
|
||||
"remote_error",
|
||||
format!("file download failed with {}", response.status()),
|
||||
));
|
||||
}
|
||||
|
||||
let bytes = response.bytes().await?;
|
||||
if bytes.is_empty() {
|
||||
return Err(ApiError::new("file_error", "Файл пустой"));
|
||||
}
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
tokio::fs::create_dir_all(parent)
|
||||
.await
|
||||
.map_err(|error| ApiError::new("file_error", error.to_string()))?;
|
||||
}
|
||||
tokio::fs::write(&path, &bytes)
|
||||
.await
|
||||
.map_err(|error| ApiError::new("file_error", error.to_string()))?;
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn normalize_remote_file_url(base_url: &str, url: &str) -> Result<reqwest::Url, ApiError> {
|
||||
if let Ok(parsed) = reqwest::Url::parse(url.trim()) {
|
||||
return Ok(parsed);
|
||||
}
|
||||
|
||||
let base = reqwest::Url::parse(&format!("{}/", base_url.trim_end_matches('/')))
|
||||
.map_err(|error| ApiError::new("file_error", error.to_string()))?;
|
||||
base.join(url.trim_start_matches('/'))
|
||||
.map_err(|error| ApiError::new("file_error", error.to_string()))
|
||||
}
|
||||
|
||||
fn remote_file_cache_path(url_path: &str) -> Result<PathBuf, ApiError> {
|
||||
let mut path = PathBuf::new();
|
||||
|
||||
for segment in url_path.split('/') {
|
||||
if segment.is_empty() || segment == "." || segment == ".." {
|
||||
continue;
|
||||
}
|
||||
|
||||
path.push(sanitize_path_segment(segment));
|
||||
}
|
||||
|
||||
if path.as_os_str().is_empty() {
|
||||
Err(ApiError::new("file_error", "URL не содержит имя файла"))
|
||||
} else {
|
||||
Ok(path)
|
||||
}
|
||||
}
|
||||
|
||||
fn sanitize_path_segment(segment: &str) -> String {
|
||||
let sanitized = segment
|
||||
.chars()
|
||||
.map(|character| match character {
|
||||
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
|
||||
_ => character,
|
||||
})
|
||||
.collect::<String>();
|
||||
|
||||
if sanitized.is_empty() {
|
||||
"file".to_string()
|
||||
} else {
|
||||
sanitized
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -64,18 +428,20 @@ pub fn run() {
|
||||
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
||||
}
|
||||
|
||||
let api = tauri::async_runtime::block_on(async {
|
||||
let state = AppState::from_config_file("app.toml").await?;
|
||||
TauriApi::new(state)
|
||||
.install(apps::installed_apps())
|
||||
.build()
|
||||
.await
|
||||
})
|
||||
.expect("failed to initialize che-tauri");
|
||||
|
||||
tauri::Builder::default()
|
||||
.manage(api)
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.setup(|app| {
|
||||
let app_data_dir = app.path().app_data_dir()?;
|
||||
std::fs::create_dir_all(&app_data_dir)?;
|
||||
let database_path = app_data_dir.join("ewa-mobile.sqlite");
|
||||
let database_url = format!("sqlite://{}?mode=rwc", database_path.to_string_lossy());
|
||||
let settings = tauri::async_runtime::block_on(load_settings(&app_data_dir))
|
||||
.map_err(|error| Box::<dyn std::error::Error>::from(error.to_string()))?;
|
||||
let api = tauri::async_runtime::block_on(build_api(database_url, settings))
|
||||
.map_err(|error| Box::<dyn std::error::Error>::from(error.to_string()))?;
|
||||
app.manage(api);
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
greet,
|
||||
che_api,
|
||||
@@ -83,7 +449,14 @@ pub fn run() {
|
||||
auth_set_token,
|
||||
auth_logout,
|
||||
auth_status,
|
||||
sync_contracts
|
||||
get_app_settings,
|
||||
update_app_settings,
|
||||
reset_app_settings,
|
||||
current_employee,
|
||||
sync_contracts,
|
||||
load_remote_file,
|
||||
ensure_remote_file,
|
||||
open_remote_file
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -4,6 +4,8 @@ use che_orm::__private::sqlx;
|
||||
use che_tauri::{ApiError, TauriApi};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::remote_http_client;
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct SyncContractsResult {
|
||||
pub synced: usize,
|
||||
@@ -18,6 +20,12 @@ struct ContractPage {
|
||||
results: Vec<RemoteContract>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ContractCategoryPage {
|
||||
links: PageLinks,
|
||||
results: Vec<RemoteContractCategory>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct PageLinks {
|
||||
next: Option<String>,
|
||||
@@ -34,6 +42,8 @@ struct RemoteContract {
|
||||
contract_type: String,
|
||||
#[serde(rename = "get_category")]
|
||||
category: String,
|
||||
#[serde(default)]
|
||||
category_id: Option<i64>,
|
||||
#[serde(rename = "get_amount_total")]
|
||||
amount_total_display: String,
|
||||
#[serde(rename = "get_amount_by_ds")]
|
||||
@@ -62,6 +72,17 @@ struct RemoteContract {
|
||||
get_employee: Option<RemoteEmployee>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RemoteContractCategory {
|
||||
id: i64,
|
||||
name: String,
|
||||
name_group: String,
|
||||
template: Option<String>,
|
||||
is_questionnair: bool,
|
||||
parent: Option<i64>,
|
||||
secure_group: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RemoteBillCost {
|
||||
#[serde(rename = "cost__sum")]
|
||||
@@ -139,18 +160,18 @@ struct RemoteEmployee {
|
||||
|
||||
pub async fn sync_contracts(
|
||||
api: &TauriApi,
|
||||
app_data_dir: PathBuf,
|
||||
_app_data_dir: PathBuf,
|
||||
) -> Result<SyncContractsResult, ApiError> {
|
||||
let state = api.state();
|
||||
let token = state
|
||||
.auth_token()
|
||||
.ok_or_else(|| ApiError::new("not_authenticated", "authentication token is missing"))?;
|
||||
let remote = state
|
||||
.config
|
||||
.remote
|
||||
.as_ref()
|
||||
.remote_config()
|
||||
.ok_or_else(|| ApiError::bad_request("sync requires [remote].base_url config"))?;
|
||||
let client = reqwest::Client::new();
|
||||
let client = remote_http_client();
|
||||
sync_contract_categories(api, &client, &token, &remote.base_url).await?;
|
||||
|
||||
let mut next_url = Some(format!(
|
||||
"{}/api/contract/?page_size=100",
|
||||
remote.base_url.trim_end_matches('/')
|
||||
@@ -182,14 +203,13 @@ pub async fn sync_contracts(
|
||||
for contract in &page.results {
|
||||
upsert_contract_dependencies(api, contract).await?;
|
||||
upsert_contract(api, contract).await?;
|
||||
let detail = fetch_contract_detail(&client, &token, &remote.base_url, contract.id).await?;
|
||||
let detail =
|
||||
fetch_contract_detail(&client, &token, &remote.base_url, contract.id).await?;
|
||||
|
||||
for application in &detail.contractapplicationfile_set {
|
||||
let local_path =
|
||||
download_application_file(&client, &token, &app_data_dir, application).await?;
|
||||
upsert_contract_application_file(api, contract.id, application, &local_path).await?;
|
||||
upsert_contract_application_file(api, contract.id, application, "").await?;
|
||||
applications += 1;
|
||||
if !local_path.is_empty() {
|
||||
if !application.scan.is_empty() {
|
||||
files_downloaded += 1;
|
||||
}
|
||||
}
|
||||
@@ -208,6 +228,74 @@ pub async fn sync_contracts(
|
||||
})
|
||||
}
|
||||
|
||||
async fn sync_contract_categories(
|
||||
api: &TauriApi,
|
||||
client: &reqwest::Client,
|
||||
token: &str,
|
||||
base_url: &str,
|
||||
) -> Result<(), ApiError> {
|
||||
let mut next_url = Some(format!(
|
||||
"{}/api/cont/category/?page_size=100",
|
||||
base_url.trim_end_matches('/')
|
||||
));
|
||||
|
||||
while let Some(url) = next_url {
|
||||
let response = client
|
||||
.get(url)
|
||||
.header(reqwest::header::AUTHORIZATION, format!("Token {token}"))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let detail = response.text().await.unwrap_or_default();
|
||||
return Err(ApiError::new(
|
||||
"remote_error",
|
||||
format!("remote category request failed with {status}: {detail}"),
|
||||
));
|
||||
}
|
||||
|
||||
let page = response.json::<ContractCategoryPage>().await?;
|
||||
for category in &page.results {
|
||||
upsert_contract_category(api, category).await?;
|
||||
}
|
||||
next_url = page.links.next;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn upsert_contract_category(
|
||||
api: &TauriApi,
|
||||
category: &RemoteContractCategory,
|
||||
) -> Result<(), ApiError> {
|
||||
sqlx::query(
|
||||
"INSERT INTO contract_category (
|
||||
id, name, name_group, template, is_questionnair, parent, secure_group
|
||||
)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
name = excluded.name,
|
||||
name_group = excluded.name_group,
|
||||
template = excluded.template,
|
||||
is_questionnair = excluded.is_questionnair,
|
||||
parent = excluded.parent,
|
||||
secure_group = excluded.secure_group",
|
||||
)
|
||||
.bind(category.id)
|
||||
.bind(&category.name)
|
||||
.bind(&category.name_group)
|
||||
.bind(category.template.as_deref())
|
||||
.bind(category.is_questionnair)
|
||||
.bind(category.parent)
|
||||
.bind(category.secure_group)
|
||||
.execute(api.state().db().pool())
|
||||
.await
|
||||
.map_err(database_error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_contract_detail(
|
||||
client: &reqwest::Client,
|
||||
token: &str,
|
||||
@@ -235,65 +323,6 @@ async fn fetch_contract_detail(
|
||||
Ok(response.json::<RemoteContractDetail>().await?)
|
||||
}
|
||||
|
||||
async fn download_application_file(
|
||||
client: &reqwest::Client,
|
||||
token: &str,
|
||||
app_data_dir: &PathBuf,
|
||||
application: &RemoteContractApplicationFile,
|
||||
) -> Result<String, ApiError> {
|
||||
if application.scan.is_empty() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
let directory = application_download_dir(app_data_dir);
|
||||
tokio::fs::create_dir_all(&directory)
|
||||
.await
|
||||
.map_err(file_error)?;
|
||||
|
||||
let file_name = format!("{}_{}", application.id, sanitize_file_name(&application.scan_name));
|
||||
let path = directory.join(file_name);
|
||||
|
||||
let response = client
|
||||
.get(&application.scan)
|
||||
.header(reqwest::header::AUTHORIZATION, format!("Token {token}"))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let detail = response.text().await.unwrap_or_default();
|
||||
return Err(ApiError::new(
|
||||
"remote_error",
|
||||
format!("file download failed with {status}: {detail}"),
|
||||
));
|
||||
}
|
||||
|
||||
let bytes = response.bytes().await?;
|
||||
tokio::fs::write(&path, bytes).await.map_err(file_error)?;
|
||||
|
||||
Ok(path.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
fn application_download_dir(app_data_dir: &PathBuf) -> PathBuf {
|
||||
app_data_dir.join("contract_applications")
|
||||
}
|
||||
|
||||
fn sanitize_file_name(file_name: &str) -> String {
|
||||
let sanitized = file_name
|
||||
.chars()
|
||||
.map(|character| match character {
|
||||
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
|
||||
_ => character,
|
||||
})
|
||||
.collect::<String>();
|
||||
|
||||
if sanitized.is_empty() {
|
||||
"application_file".to_string()
|
||||
} else {
|
||||
sanitized
|
||||
}
|
||||
}
|
||||
|
||||
async fn upsert_contract_dependencies(
|
||||
api: &TauriApi,
|
||||
contract: &RemoteContract,
|
||||
@@ -379,7 +408,7 @@ async fn upsert_contract_dependencies(
|
||||
async fn upsert_contract(api: &TauriApi, contract: &RemoteContract) -> Result<(), ApiError> {
|
||||
sqlx::query(
|
||||
"INSERT INTO contractapp (
|
||||
id, name, number, absolute_url, contract_type, category, amount_total_display,
|
||||
id, name, number, absolute_url, contract_type, category, category_id, amount_total_display,
|
||||
amount_by_ds, comment, date, status_name, status, nds, name_of_product,
|
||||
counterparty_name, amount, month_pay, avans_pay, amount_total, estimate_nds_cost,
|
||||
estimate_nds_cert, bill_cost_sum, bill_paid_sum, income_total, arrears,
|
||||
@@ -387,7 +416,7 @@ async fn upsert_contract(api: &TauriApi, contract: &RemoteContract) -> Result<()
|
||||
)
|
||||
VALUES (
|
||||
?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15,
|
||||
?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29
|
||||
?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29, ?30
|
||||
)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
name = excluded.name,
|
||||
@@ -395,6 +424,7 @@ async fn upsert_contract(api: &TauriApi, contract: &RemoteContract) -> Result<()
|
||||
absolute_url = excluded.absolute_url,
|
||||
contract_type = excluded.contract_type,
|
||||
category = excluded.category,
|
||||
category_id = excluded.category_id,
|
||||
amount_total_display = excluded.amount_total_display,
|
||||
amount_by_ds = excluded.amount_by_ds,
|
||||
comment = excluded.comment,
|
||||
@@ -425,6 +455,7 @@ async fn upsert_contract(api: &TauriApi, contract: &RemoteContract) -> Result<()
|
||||
.bind(&contract.absolute_url)
|
||||
.bind(&contract.contract_type)
|
||||
.bind(&contract.category)
|
||||
.bind(contract.category_id)
|
||||
.bind(&contract.amount_total_display)
|
||||
.bind(&contract.amount_by_ds)
|
||||
.bind(&contract.comment)
|
||||
@@ -509,7 +540,3 @@ async fn upsert_contract_application_file(
|
||||
fn database_error(error: sqlx::Error) -> ApiError {
|
||||
ApiError::new("database_error", error.to_string())
|
||||
}
|
||||
|
||||
fn file_error(error: impl std::fmt::Display) -> ApiError {
|
||||
ApiError::new("file_error", error.to_string())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useAuth } from "./composables/useAuth";
|
||||
import { useAuth } from "./shared/auth/useAuth";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -9,8 +9,24 @@ const { logout } = useAuth();
|
||||
|
||||
const activeTab = computed({
|
||||
get() {
|
||||
if (route.path.startsWith("/documents")) {
|
||||
return "/documents";
|
||||
}
|
||||
|
||||
if (route.path.startsWith("/bills")) {
|
||||
return "/documents";
|
||||
}
|
||||
|
||||
if (route.path.startsWith("/contracts")) {
|
||||
return "/contracts";
|
||||
return "/documents";
|
||||
}
|
||||
|
||||
if (route.path.startsWith("/memos")) {
|
||||
return "/documents";
|
||||
}
|
||||
|
||||
if (route.path.startsWith("/settings")) {
|
||||
return "/settings";
|
||||
}
|
||||
|
||||
return route.path.startsWith("/users") ? "/users" : "/tasks";
|
||||
@@ -47,118 +63,26 @@ async function logoutAndRedirect() {
|
||||
<router-view />
|
||||
</main>
|
||||
|
||||
<van-tabbar v-if="showShellNavigation" v-model="activeTab" route placeholder safe-area-inset-bottom>
|
||||
<van-tabbar-item to="/tasks" name="/tasks" icon="todo-list-o">Задачи</van-tabbar-item>
|
||||
<van-tabbar-item to="/contracts" name="/contracts" icon="orders-o">Контракты</van-tabbar-item>
|
||||
<van-tabbar-item to="/users" name="/users" icon="friends-o">Пользователи</van-tabbar-item>
|
||||
<van-tabbar
|
||||
v-if="showShellNavigation"
|
||||
v-model="activeTab"
|
||||
route
|
||||
placeholder
|
||||
safe-area-inset-bottom
|
||||
>
|
||||
<van-tabbar-item to="/tasks" name="/tasks" icon="todo-list-o"
|
||||
>Задачи</van-tabbar-item
|
||||
>
|
||||
<van-tabbar-item to="/documents" name="/documents" icon="description-o"
|
||||
>Документы</van-tabbar-item
|
||||
>
|
||||
<van-tabbar-item to="/users" name="/users" icon="friends-o"
|
||||
>Пользователи</van-tabbar-item
|
||||
>
|
||||
<van-tabbar-item to="/settings" name="/settings" icon="setting-o"
|
||||
>Настройки</van-tabbar-item
|
||||
>
|
||||
</van-tabbar>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
font-family:
|
||||
Inter,
|
||||
ui-sans-serif,
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
color: #172033;
|
||||
background: #f7f8fa;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
.page {
|
||||
box-sizing: border-box;
|
||||
width: min(760px, 100%);
|
||||
margin: 0 auto;
|
||||
padding: 24px 14px 40px;
|
||||
}
|
||||
|
||||
.notice {
|
||||
margin-bottom: 12px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.card {
|
||||
overflow: hidden;
|
||||
margin-bottom: 14px;
|
||||
border-radius: 18px;
|
||||
background: #fff;
|
||||
box-shadow: 0 12px 40px rgba(36, 42, 56, 0.08);
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
padding: 14px 16px 16px;
|
||||
}
|
||||
|
||||
.list-card {
|
||||
padding: 14px 0 16px;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 0 16px 12px;
|
||||
}
|
||||
|
||||
.list-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stacked-actions {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.state {
|
||||
justify-content: center;
|
||||
padding: 34px 0;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
padding: 14px 0 16px;
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
padding: 16px 16px 0;
|
||||
}
|
||||
|
||||
.employee-popup {
|
||||
box-sizing: border-box;
|
||||
max-height: 78vh;
|
||||
padding: 16px 0 22px;
|
||||
}
|
||||
|
||||
.employee-popup-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 0 16px 12px;
|
||||
}
|
||||
|
||||
.employee-avatar {
|
||||
margin-right: 10px;
|
||||
}
|
||||
</style>
|
||||
<style></style>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { createRouter, createWebHashHistory } from "vue-router";
|
||||
import { isAuthenticated, useAuth } from "../../shared/auth/useAuth";
|
||||
import { loginRoute } from "../../apps/auth/routes";
|
||||
import { documentsRoute } from "../../apps/documents/routes";
|
||||
import { contractsRoutes } from "../../apps/contracts/routes";
|
||||
import { memosRoutes } from "../../apps/memos/routes";
|
||||
import { settingsRoute } from "../../apps/settings/routes";
|
||||
import { supplyRoutes } from "../../apps/supply/routes";
|
||||
import { tasksRoutes } from "../../apps/tasks/routes";
|
||||
import { usersRoutes } from "../../apps/users/routes";
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
redirect: "/documents",
|
||||
},
|
||||
...tasksRoutes,
|
||||
...usersRoutes,
|
||||
...contractsRoutes,
|
||||
...memosRoutes,
|
||||
...supplyRoutes,
|
||||
documentsRoute,
|
||||
settingsRoute,
|
||||
loginRoute,
|
||||
],
|
||||
});
|
||||
|
||||
router.beforeEach(async (to) => {
|
||||
const { restoreToken } = useAuth();
|
||||
await restoreToken();
|
||||
|
||||
if (to.meta.requiresAuth && !isAuthenticated()) {
|
||||
return { path: "/login", query: { redirect: to.fullPath } };
|
||||
}
|
||||
|
||||
if (to.path === "/login" && isAuthenticated()) {
|
||||
return "/documents";
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import LoginView from "./views/LoginView.vue";
|
||||
|
||||
export const loginRoute = {
|
||||
path: "/login",
|
||||
name: "login",
|
||||
component: LoginView,
|
||||
meta: { title: "Вход" },
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { showToast } from "vant";
|
||||
import { useAuth } from "../composables/useAuth";
|
||||
import { useAuth } from "../../../shared/auth/useAuth";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -82,6 +82,17 @@ function errorMessage(err: unknown) {
|
||||
<van-button block round type="primary" native-type="submit" :loading="loading">
|
||||
Войти
|
||||
</van-button>
|
||||
<van-button
|
||||
block
|
||||
round
|
||||
plain
|
||||
type="primary"
|
||||
native-type="button"
|
||||
:disabled="loading"
|
||||
@click="router.push('/settings')"
|
||||
>
|
||||
Настройки сервера
|
||||
</van-button>
|
||||
</div>
|
||||
</van-form>
|
||||
</template>
|
||||
@@ -0,0 +1,475 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { taskApi } from "../../../generated/api";
|
||||
import type { Employee, Task, TaskListParams } from "../../../generated/models";
|
||||
import { useModelApi } from "../../../shared/composables/useModelApi";
|
||||
import RemoteImage from "../../../shared/components/RemoteImage.vue";
|
||||
|
||||
type ApprovalMessage = {
|
||||
id: number;
|
||||
author: Employee;
|
||||
recipient: Employee;
|
||||
text: string;
|
||||
date: string;
|
||||
status: string;
|
||||
task: number;
|
||||
};
|
||||
|
||||
type ApprovalTask = Omit<Task, "message_set"> & {
|
||||
message_set?: ApprovalMessage[] | null;
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
contractId: number;
|
||||
}>();
|
||||
|
||||
const {
|
||||
items: tasks,
|
||||
loading,
|
||||
error,
|
||||
load,
|
||||
} = useModelApi(taskApi, {
|
||||
loadErrorMessage: "Не удалось загрузить поручения",
|
||||
autoLoad: false,
|
||||
autoLoadOnFilterChange: false,
|
||||
});
|
||||
|
||||
const expandedTasks = ref<Record<number, boolean>>({});
|
||||
|
||||
const hasTasks = computed(() => tasks.value.length > 0);
|
||||
|
||||
function taskTitle(task: Task | null | undefined) {
|
||||
return task?.text?.trim() || task?.result?.trim() || `Задача #${task?.id ?? ""}`;
|
||||
}
|
||||
|
||||
function personName(
|
||||
person: { short_name?: string | null; name?: string | null } | null,
|
||||
) {
|
||||
if (!person) {
|
||||
return "не указан";
|
||||
}
|
||||
|
||||
return person.short_name ?? person.name ?? "не указан";
|
||||
}
|
||||
|
||||
function personAvatar(person: { avatar_small?: string | null } | null) {
|
||||
return person?.avatar_small ?? "";
|
||||
}
|
||||
|
||||
function taskMessages(task: Task | null | undefined) {
|
||||
return (task as ApprovalTask | null | undefined)?.message_set ?? [];
|
||||
}
|
||||
|
||||
function isHistoryExpanded(taskId: number) {
|
||||
return expandedTasks.value[taskId] ?? false;
|
||||
}
|
||||
|
||||
function toggleHistory(taskId: number) {
|
||||
expandedTasks.value = {
|
||||
...expandedTasks.value,
|
||||
[taskId]: !isHistoryExpanded(taskId),
|
||||
};
|
||||
}
|
||||
|
||||
function visibleMessages(task: Task | null | undefined) {
|
||||
const messages = taskMessages(task);
|
||||
|
||||
if (isHistoryExpanded(task?.id ?? 0)) {
|
||||
return messages;
|
||||
}
|
||||
|
||||
return messages.length > 0 ? [messages[messages.length - 1]] : [];
|
||||
}
|
||||
|
||||
function statusType(statusClass: string) {
|
||||
if (/success|done|complete|green/i.test(statusClass)) {
|
||||
return "success";
|
||||
}
|
||||
|
||||
if (/warning|pending|wait|yellow|orange/i.test(statusClass)) {
|
||||
return "warning";
|
||||
}
|
||||
|
||||
if (/danger|error|red|fail/i.test(statusClass)) {
|
||||
return "danger";
|
||||
}
|
||||
|
||||
return "primary";
|
||||
}
|
||||
|
||||
function statusTone(statusClass: string) {
|
||||
if (/success|done|complete|green/i.test(statusClass)) {
|
||||
return "success";
|
||||
}
|
||||
|
||||
if (/warning|pending|wait|yellow|orange/i.test(statusClass)) {
|
||||
return "warning";
|
||||
}
|
||||
|
||||
if (/danger|error|red|fail/i.test(statusClass)) {
|
||||
return "danger";
|
||||
}
|
||||
|
||||
return "primary";
|
||||
}
|
||||
|
||||
function loadTasks() {
|
||||
const params = {
|
||||
contract: String(props.contractId),
|
||||
ordering: "-id",
|
||||
} as TaskListParams;
|
||||
|
||||
return load(params);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.contractId,
|
||||
() => {
|
||||
void loadTasks();
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
void loadTasks();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="approval-tasks">
|
||||
<van-notice-bar
|
||||
v-if="error"
|
||||
class="approval-tasks__error"
|
||||
color="#991b1b"
|
||||
background="#fee2e2"
|
||||
left-icon="warning-o"
|
||||
wrapable
|
||||
:scrollable="false"
|
||||
:text="error"
|
||||
/>
|
||||
|
||||
<van-loading v-if="loading" class="approval-tasks__state" type="spinner">
|
||||
Загрузка поручений...
|
||||
</van-loading>
|
||||
|
||||
<van-empty v-else-if="!hasTasks" description="Поручений пока нет" />
|
||||
|
||||
<div v-else class="approval-task-list">
|
||||
<article
|
||||
v-for="task in tasks"
|
||||
:key="task.id"
|
||||
:class="[
|
||||
'approval-task-card',
|
||||
`approval-task-card--${statusTone(task.get_status_class)}`,
|
||||
]"
|
||||
>
|
||||
<div class="approval-task-head">
|
||||
<div class="approval-task-head__main">
|
||||
<div class="approval-task-title">{{ taskTitle(task) }}</div>
|
||||
<div class="approval-task-meta">
|
||||
ID {{ task.id }} · {{ task.deadline }}
|
||||
</div>
|
||||
</div>
|
||||
<van-tag plain :type="statusType(task.get_status_class)">
|
||||
{{ task.get_status }}
|
||||
</van-tag>
|
||||
</div>
|
||||
|
||||
<van-cell-group inset>
|
||||
<div class="approval-person-list">
|
||||
<div class="approval-person-item">
|
||||
<RemoteImage
|
||||
class="approval-person-avatar"
|
||||
round
|
||||
width="32"
|
||||
height="32"
|
||||
:src="personAvatar(task.doer)"
|
||||
/>
|
||||
<div class="approval-person-body">
|
||||
<div class="approval-person-role">Исполнитель</div>
|
||||
<div class="approval-person-name">
|
||||
{{ personName(task.doer) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="approval-person-item">
|
||||
<RemoteImage
|
||||
class="approval-person-avatar"
|
||||
round
|
||||
width="32"
|
||||
height="32"
|
||||
:src="personAvatar(task.author)"
|
||||
/>
|
||||
<div class="approval-person-body">
|
||||
<div class="approval-person-role">Автор</div>
|
||||
<div class="approval-person-name">
|
||||
{{ personName(task.author) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="approval-person-item">
|
||||
<RemoteImage
|
||||
class="approval-person-avatar"
|
||||
round
|
||||
width="32"
|
||||
height="32"
|
||||
:src="personAvatar(task.responsible)"
|
||||
/>
|
||||
<div class="approval-person-body">
|
||||
<div class="approval-person-role">Ответственный</div>
|
||||
<div class="approval-person-name">
|
||||
{{ personName(task.responsible) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-cell-group>
|
||||
|
||||
<div class="approval-task-messages">
|
||||
<div class="approval-task-messages__title">Сообщения</div>
|
||||
|
||||
<van-empty
|
||||
v-if="!taskMessages(task).length"
|
||||
description="Сообщений пока нет"
|
||||
image-size="64"
|
||||
/>
|
||||
|
||||
<div v-else class="approval-message-list">
|
||||
<article
|
||||
v-for="message in visibleMessages(task)"
|
||||
:key="message.id"
|
||||
:class="[
|
||||
'approval-message-item',
|
||||
!isHistoryExpanded(task.id) &&
|
||||
message.id === taskMessages(task)[taskMessages(task).length - 1]?.id
|
||||
? 'approval-message-item--latest'
|
||||
: '',
|
||||
]"
|
||||
@click="
|
||||
message.id === taskMessages(task)[taskMessages(task).length - 1]?.id &&
|
||||
!isHistoryExpanded(task.id)
|
||||
? toggleHistory(task.id)
|
||||
: undefined
|
||||
"
|
||||
>
|
||||
<RemoteImage
|
||||
class="approval-message-avatar"
|
||||
round
|
||||
width="36"
|
||||
height="36"
|
||||
:src="message.author.avatar_small"
|
||||
/>
|
||||
|
||||
<div class="approval-message-body">
|
||||
<div class="approval-message-head">
|
||||
<div class="approval-message-author">
|
||||
{{ personName(message.author) }}
|
||||
</div>
|
||||
<div class="approval-message-meta">
|
||||
{{ message.date }} · {{ message.status }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="approval-message-text">{{ message.text }}</div>
|
||||
<div class="approval-message-recipient">
|
||||
Кому: {{ personName(message.recipient) }}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<van-button
|
||||
v-if="taskMessages(task).length > 1"
|
||||
class="approval-message-toggle"
|
||||
size="small"
|
||||
round
|
||||
plain
|
||||
type="primary"
|
||||
@click.stop="toggleHistory(task.id)"
|
||||
>
|
||||
<van-icon
|
||||
:name="isHistoryExpanded(task.id) ? 'arrow-up' : 'arrow-down'"
|
||||
/>
|
||||
</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.approval-tasks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 8px 0 0;
|
||||
}
|
||||
|
||||
.approval-tasks__state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.approval-task-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.approval-task-card {
|
||||
overflow: hidden;
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
box-shadow: 0 8px 24px rgba(36, 42, 56, 0.08);
|
||||
}
|
||||
|
||||
.approval-task-card--success {
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
.approval-task-card--warning {
|
||||
background: #fffbeb;
|
||||
}
|
||||
|
||||
.approval-task-card--danger {
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.approval-task-card--primary {
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.approval-task-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 16px 10px;
|
||||
}
|
||||
|
||||
.approval-task-head__main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.approval-task-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.approval-task-meta {
|
||||
margin-top: 4px;
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.approval-task-messages {
|
||||
padding: 12px 16px 16px;
|
||||
}
|
||||
|
||||
.approval-person-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.approval-person-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.approval-person-avatar {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.approval-person-body {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.approval-person-role {
|
||||
color: #6b7280;
|
||||
font-size: 10px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.approval-person-name {
|
||||
margin-top: 1px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.35;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.approval-task-messages__title {
|
||||
margin-bottom: 10px;
|
||||
color: #374151;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.approval-message-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.approval-message-item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.approval-message-item--latest {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.approval-message-avatar {
|
||||
flex: none;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.approval-message-body {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.approval-message-head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.approval-message-author {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.approval-message-meta,
|
||||
.approval-message-recipient {
|
||||
color: #6b7280;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.approval-message-toggle {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-top: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.approval-message-text {
|
||||
margin-top: 4px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,140 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { contractCategoryApi } from "../../../generated/api";
|
||||
import type { ContractCategoryListParams } from "../../../generated/models";
|
||||
import { useModelApi } from "../../../shared/composables/useModelApi";
|
||||
|
||||
const PAGE_SIZE = 30;
|
||||
|
||||
const selectedCategoryId = defineModel<number>({ required: true });
|
||||
const search = ref("");
|
||||
const showSelector = ref(false);
|
||||
|
||||
const {
|
||||
items: categories,
|
||||
filters,
|
||||
loading,
|
||||
} = useModelApi(contractCategoryApi, {
|
||||
defaultListParams: { ordering: "id", limit: PAGE_SIZE, offset: 0 } as ContractCategoryListParams,
|
||||
loadErrorMessage: "Не удалось загрузить категории",
|
||||
cleanListParams(params) {
|
||||
params.name__contains = params.name__contains?.trim() || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const selectedCategory = computed(() =>
|
||||
categories.value.find((category) => category.id === selectedCategoryId.value),
|
||||
);
|
||||
const selectedCategoryName = computed(() => selectedCategory.value?.name ?? "все");
|
||||
|
||||
function openSelector() {
|
||||
search.value = "";
|
||||
showSelector.value = true;
|
||||
}
|
||||
|
||||
function selectCategory(id: number) {
|
||||
selectedCategoryId.value = id;
|
||||
showSelector.value = false;
|
||||
}
|
||||
|
||||
watch(search, (value) => {
|
||||
filters.name__contains = value.trim();
|
||||
filters.offset = 0;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button class="entity-select" type="button" @click="openSelector">
|
||||
<span>{{ selectedCategoryName }}</span>
|
||||
<van-icon name="arrow" />
|
||||
</button>
|
||||
|
||||
<van-popup v-model:show="showSelector" round position="bottom" class="entity-popup">
|
||||
<div class="entity-popup-header">
|
||||
<h2>Выберите категорию</h2>
|
||||
<van-button size="small" type="primary" plain @click="selectCategory(0)">Все</van-button>
|
||||
</div>
|
||||
|
||||
<div class="entity-popup-body">
|
||||
<van-search v-model="search" placeholder="Поиск по категории" />
|
||||
|
||||
<van-loading v-if="loading" class="entity-state" type="spinner">Загрузка...</van-loading>
|
||||
|
||||
<template v-else>
|
||||
<van-cell-group inset>
|
||||
<van-cell
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
:title="category.name"
|
||||
:label="category.name_group || `ID: ${category.id}`"
|
||||
clickable
|
||||
center
|
||||
@click="selectCategory(category.id)"
|
||||
>
|
||||
<template #right-icon>
|
||||
<van-icon v-if="selectedCategoryId === category.id" name="success" color="#1989fa" />
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
|
||||
<van-empty v-if="categories.length === 0" description="Категории не найдены" />
|
||||
</template>
|
||||
</div>
|
||||
</van-popup>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.entity-popup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 70vh;
|
||||
max-height: 70vh;
|
||||
overflow: hidden;
|
||||
padding: 18px 0 16px;
|
||||
}
|
||||
|
||||
.entity-popup-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 0 18px 10px;
|
||||
}
|
||||
|
||||
.entity-popup-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.entity-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 36px 0;
|
||||
}
|
||||
|
||||
.entity-popup-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.entity-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
color: #323233;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
line-height: 24px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.entity-select .van-icon {
|
||||
color: #969799;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { counterpartyApi } from "../generated/api";
|
||||
import type { CounterpartyListParams } from "../generated/models";
|
||||
import { useModelApi } from "../composables/useModelApi";
|
||||
import { counterpartyApi } from "../../../generated/api";
|
||||
import type { CounterpartyListParams } from "../../../generated/models";
|
||||
import { useModelApi } from "../../../shared/composables/useModelApi";
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
@@ -58,40 +58,46 @@ watch(search, (value) => {
|
||||
<van-button size="small" type="primary" plain @click="selectCounterparty(0)">Все</van-button>
|
||||
</div>
|
||||
|
||||
<van-search v-model="search" placeholder="Поиск по имени" />
|
||||
<div class="counterparty-popup-body">
|
||||
<van-search v-model="search" placeholder="Поиск по имени" />
|
||||
|
||||
<van-loading v-if="loading" class="counterparty-state" type="spinner">Загрузка...</van-loading>
|
||||
<van-loading v-if="loading" class="counterparty-state" type="spinner">Загрузка...</van-loading>
|
||||
|
||||
<template v-else>
|
||||
<van-cell-group inset>
|
||||
<van-cell
|
||||
v-for="counterparty in counterparties"
|
||||
:key="counterparty.id"
|
||||
:title="counterparty.name"
|
||||
:label="`ID: ${counterparty.id}`"
|
||||
clickable
|
||||
center
|
||||
@click="selectCounterparty(counterparty.id)"
|
||||
>
|
||||
<template #right-icon>
|
||||
<van-icon
|
||||
v-if="selectedCounterpartyId === counterparty.id"
|
||||
name="success"
|
||||
color="#1989fa"
|
||||
/>
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
<template v-else>
|
||||
<van-cell-group inset>
|
||||
<van-cell
|
||||
v-for="counterparty in counterparties"
|
||||
:key="counterparty.id"
|
||||
:title="counterparty.name"
|
||||
:label="`ID: ${counterparty.id}`"
|
||||
clickable
|
||||
center
|
||||
@click="selectCounterparty(counterparty.id)"
|
||||
>
|
||||
<template #right-icon>
|
||||
<van-icon
|
||||
v-if="selectedCounterpartyId === counterparty.id"
|
||||
name="success"
|
||||
color="#1989fa"
|
||||
/>
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
|
||||
<van-empty v-if="counterparties.length === 0" description="Контрагенты не найдены" />
|
||||
</template>
|
||||
<van-empty v-if="counterparties.length === 0" description="Контрагенты не найдены" />
|
||||
</template>
|
||||
</div>
|
||||
</van-popup>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.counterparty-popup {
|
||||
min-height: 55vh;
|
||||
padding: 18px 0 28px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 70vh;
|
||||
max-height: 70vh;
|
||||
overflow: hidden;
|
||||
padding: 18px 0 16px;
|
||||
}
|
||||
|
||||
.counterparty-popup-header {
|
||||
@@ -113,6 +119,12 @@ watch(search, (value) => {
|
||||
padding: 36px 0;
|
||||
}
|
||||
|
||||
.counterparty-popup-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.counterparty-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -0,0 +1,138 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { frcApi } from "../../../generated/api";
|
||||
import type { FrcListParams } from "../../../generated/models";
|
||||
import { useModelApi } from "../../../shared/composables/useModelApi";
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
const selectedFrcId = defineModel<number>({ required: true });
|
||||
const search = ref("");
|
||||
const showSelector = ref(false);
|
||||
|
||||
const {
|
||||
items: frcs,
|
||||
filters,
|
||||
loading,
|
||||
} = useModelApi(frcApi, {
|
||||
defaultListParams: { ordering: "id", limit: PAGE_SIZE, offset: 0 } as FrcListParams,
|
||||
loadErrorMessage: "Не удалось загрузить ФРЦ",
|
||||
cleanListParams(params) {
|
||||
params.name__contains = params.name__contains?.trim() || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const selectedFrc = computed(() => frcs.value.find((frc) => frc.id === selectedFrcId.value));
|
||||
const selectedFrcName = computed(() => selectedFrc.value?.name ?? "все");
|
||||
|
||||
function openSelector() {
|
||||
search.value = "";
|
||||
showSelector.value = true;
|
||||
}
|
||||
|
||||
function selectFrc(id: number) {
|
||||
selectedFrcId.value = id;
|
||||
showSelector.value = false;
|
||||
}
|
||||
|
||||
watch(search, (value) => {
|
||||
filters.name__contains = value.trim();
|
||||
filters.offset = 0;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button class="entity-select" type="button" @click="openSelector">
|
||||
<span>{{ selectedFrcName }}</span>
|
||||
<van-icon name="arrow" />
|
||||
</button>
|
||||
|
||||
<van-popup v-model:show="showSelector" round position="bottom" class="entity-popup">
|
||||
<div class="entity-popup-header">
|
||||
<h2>Выберите ФРЦ</h2>
|
||||
<van-button size="small" type="primary" plain @click="selectFrc(0)">Все</van-button>
|
||||
</div>
|
||||
|
||||
<div class="entity-popup-body">
|
||||
<van-search v-model="search" placeholder="Поиск по названию" />
|
||||
|
||||
<van-loading v-if="loading" class="entity-state" type="spinner">Загрузка...</van-loading>
|
||||
|
||||
<template v-else>
|
||||
<van-cell-group inset>
|
||||
<van-cell
|
||||
v-for="frc in frcs"
|
||||
:key="frc.id"
|
||||
:title="frc.name"
|
||||
:label="`Баланс: ${frc.balance}`"
|
||||
clickable
|
||||
center
|
||||
@click="selectFrc(frc.id)"
|
||||
>
|
||||
<template #right-icon>
|
||||
<van-icon v-if="selectedFrcId === frc.id" name="success" color="#1989fa" />
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
|
||||
<van-empty v-if="frcs.length === 0" description="ФРЦ не найдены" />
|
||||
</template>
|
||||
</div>
|
||||
</van-popup>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.entity-popup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 70vh;
|
||||
max-height: 70vh;
|
||||
overflow: hidden;
|
||||
padding: 18px 0 16px;
|
||||
}
|
||||
|
||||
.entity-popup-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 0 18px 10px;
|
||||
}
|
||||
|
||||
.entity-popup-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.entity-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 36px 0;
|
||||
}
|
||||
|
||||
.entity-popup-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.entity-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
color: #323233;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
line-height: 24px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.entity-select .van-icon {
|
||||
color: #969799;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,262 @@
|
||||
<script setup lang="ts">
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import {
|
||||
GlobalWorkerOptions,
|
||||
getDocument,
|
||||
type PDFDocumentLoadingTask,
|
||||
type PDFDocumentProxy,
|
||||
type RenderTask,
|
||||
} from "pdfjs-dist";
|
||||
import { computed, nextTick, onBeforeUnmount, ref, shallowRef, watch } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
scanUrl: string;
|
||||
title: string;
|
||||
}>();
|
||||
|
||||
const show = defineModel<boolean>("show", { required: true });
|
||||
|
||||
GlobalWorkerOptions.workerSrc = new URL(
|
||||
"pdfjs-dist/build/pdf.worker.min.mjs",
|
||||
import.meta.url,
|
||||
).toString();
|
||||
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
||||
const pdfDocument = shallowRef<PDFDocumentProxy | null>(null);
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
const pageNumber = ref(1);
|
||||
const pageCount = ref(0);
|
||||
const scale = ref(1.1);
|
||||
|
||||
let loadingTask: PDFDocumentLoadingTask | null = null;
|
||||
let renderTask: RenderTask | null = null;
|
||||
const pageInfo = computed(() => `${pageNumber.value} / ${pageCount.value}`);
|
||||
const canGoBack = computed(() => pageNumber.value > 1);
|
||||
const canGoForward = computed(() => pageNumber.value < pageCount.value);
|
||||
|
||||
async function loadDocument() {
|
||||
resetDocument();
|
||||
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
|
||||
try {
|
||||
const bytes = await invoke<number[]>("load_remote_file", {
|
||||
url: props.scanUrl,
|
||||
});
|
||||
|
||||
if (!bytes.length) {
|
||||
throw new Error("Файл пустой");
|
||||
}
|
||||
|
||||
const task = getDocument({ data: new Uint8Array(bytes) });
|
||||
loadingTask = task;
|
||||
|
||||
const document = await task.promise;
|
||||
if (loadingTask !== task) {
|
||||
await document.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
pdfDocument.value = document;
|
||||
pageCount.value = document.numPages;
|
||||
pageNumber.value = 1;
|
||||
scale.value = 1.1;
|
||||
loading.value = false;
|
||||
await nextTick();
|
||||
await renderPage();
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : "Не удалось открыть PDF";
|
||||
loading.value = false;
|
||||
if (loadingTask) {
|
||||
loadingTask = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function renderPage() {
|
||||
const document = pdfDocument.value;
|
||||
const canvas = canvasRef.value;
|
||||
|
||||
if (!document || !canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderTask?.cancel();
|
||||
|
||||
const page = await document.getPage(pageNumber.value);
|
||||
const viewport = page.getViewport({ scale: scale.value });
|
||||
const context = canvas.getContext("2d");
|
||||
|
||||
if (!context) {
|
||||
error.value = "Не удалось создать canvas context";
|
||||
return;
|
||||
}
|
||||
|
||||
const devicePixelRatio = window.devicePixelRatio || 1;
|
||||
canvas.width = Math.floor(viewport.width * devicePixelRatio);
|
||||
canvas.height = Math.floor(viewport.height * devicePixelRatio);
|
||||
canvas.style.width = `${viewport.width}px`;
|
||||
canvas.style.height = `${viewport.height}px`;
|
||||
|
||||
context.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
|
||||
|
||||
renderTask = page.render({ canvas: canvas, canvasContext: context, viewport });
|
||||
|
||||
try {
|
||||
await renderTask.promise;
|
||||
} catch (err) {
|
||||
if (!(err instanceof Error) || err.name !== "RenderingCancelledException") {
|
||||
error.value = err instanceof Error ? err.message : "Не удалось отрисовать PDF";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetDocument() {
|
||||
renderTask?.cancel();
|
||||
renderTask = null;
|
||||
loadingTask?.destroy().catch(() => undefined);
|
||||
loadingTask = null;
|
||||
pdfDocument.value?.destroy().catch(() => undefined);
|
||||
pdfDocument.value = null;
|
||||
pageCount.value = 0;
|
||||
pageNumber.value = 1;
|
||||
loading.value = false;
|
||||
error.value = "";
|
||||
}
|
||||
|
||||
async function goBack() {
|
||||
if (canGoBack.value) {
|
||||
pageNumber.value -= 1;
|
||||
await nextTick();
|
||||
await renderPage();
|
||||
}
|
||||
}
|
||||
|
||||
async function goForward() {
|
||||
if (canGoForward.value) {
|
||||
pageNumber.value += 1;
|
||||
await nextTick();
|
||||
await renderPage();
|
||||
}
|
||||
}
|
||||
|
||||
async function zoomIn() {
|
||||
scale.value = Math.min(scale.value + 0.2, 3);
|
||||
await nextTick();
|
||||
await renderPage();
|
||||
}
|
||||
|
||||
async function zoomOut() {
|
||||
scale.value = Math.max(scale.value - 0.2, 0.5);
|
||||
await nextTick();
|
||||
await renderPage();
|
||||
}
|
||||
|
||||
watch([show, () => props.scanUrl], async ([visible]) => {
|
||||
if (visible) {
|
||||
await nextTick();
|
||||
await loadDocument();
|
||||
} else {
|
||||
resetDocument();
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resetDocument();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<van-popup v-model:show="show" round position="bottom" class="pdf-preview">
|
||||
<div class="pdf-preview__header">
|
||||
<div class="pdf-preview__title">{{ title }}</div>
|
||||
<van-button size="small" plain @click="show = false">Закрыть</van-button>
|
||||
</div>
|
||||
|
||||
<div class="pdf-preview__toolbar">
|
||||
<van-button size="small" plain :disabled="!canGoBack" @click="goBack">
|
||||
Назад
|
||||
</van-button>
|
||||
<div class="pdf-preview__page-info">{{ pageInfo }}</div>
|
||||
<van-button size="small" plain :disabled="!canGoForward" @click="goForward">
|
||||
Вперед
|
||||
</van-button>
|
||||
<van-button size="small" plain @click="zoomOut">-</van-button>
|
||||
<van-button size="small" plain @click="zoomIn">+</van-button>
|
||||
</div>
|
||||
|
||||
<div class="pdf-preview__body">
|
||||
<van-loading v-if="loading">Загрузка PDF...</van-loading>
|
||||
<van-empty v-else-if="error" :description="error" />
|
||||
<div v-else class="pdf-preview__canvas-wrap">
|
||||
<canvas ref="canvasRef" class="pdf-preview__canvas" />
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pdf-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 92vh;
|
||||
background: #f5f7fb;
|
||||
}
|
||||
|
||||
.pdf-preview__header,
|
||||
.pdf-preview__toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.pdf-preview__header {
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #eef0f4;
|
||||
}
|
||||
|
||||
.pdf-preview__title {
|
||||
min-width: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pdf-preview__toolbar {
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
border-bottom: 1px solid #eef0f4;
|
||||
}
|
||||
|
||||
.pdf-preview__page-info {
|
||||
min-width: 72px;
|
||||
text-align: center;
|
||||
color: #636b74;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.pdf-preview__body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.pdf-preview__canvas-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pdf-preview__canvas {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
background: #fff;
|
||||
box-shadow: 0 8px 28px rgba(15, 23, 42, 0.16);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,142 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { projectApi } from "../../../generated/api";
|
||||
import type { ProjectListParams } from "../../../generated/models";
|
||||
import { useModelApi } from "../../../shared/composables/useModelApi";
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
const selectedProjectId = defineModel<number>({ required: true });
|
||||
const search = ref("");
|
||||
const showSelector = ref(false);
|
||||
|
||||
const {
|
||||
items: projects,
|
||||
filters,
|
||||
loading,
|
||||
} = useModelApi(projectApi, {
|
||||
defaultListParams: { ordering: "id", limit: PAGE_SIZE, offset: 0 } as ProjectListParams,
|
||||
loadErrorMessage: "Не удалось загрузить проекты",
|
||||
cleanListParams(params) {
|
||||
params.name__contains = params.name__contains?.trim() || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const selectedProject = computed(() =>
|
||||
projects.value.find((project) => project.id === selectedProjectId.value),
|
||||
);
|
||||
const selectedProjectName = computed(
|
||||
() => selectedProject.value?.short_name || selectedProject.value?.name || "все",
|
||||
);
|
||||
|
||||
function openSelector() {
|
||||
search.value = "";
|
||||
showSelector.value = true;
|
||||
}
|
||||
|
||||
function selectProject(id: number) {
|
||||
selectedProjectId.value = id;
|
||||
showSelector.value = false;
|
||||
}
|
||||
|
||||
watch(search, (value) => {
|
||||
filters.name__contains = value.trim();
|
||||
filters.offset = 0;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button class="entity-select" type="button" @click="openSelector">
|
||||
<span>{{ selectedProjectName }}</span>
|
||||
<van-icon name="arrow" />
|
||||
</button>
|
||||
|
||||
<van-popup v-model:show="showSelector" round position="bottom" class="entity-popup">
|
||||
<div class="entity-popup-header">
|
||||
<h2>Выберите проект</h2>
|
||||
<van-button size="small" type="primary" plain @click="selectProject(0)">Все</van-button>
|
||||
</div>
|
||||
|
||||
<div class="entity-popup-body">
|
||||
<van-search v-model="search" placeholder="Поиск по проекту" />
|
||||
|
||||
<van-loading v-if="loading" class="entity-state" type="spinner">Загрузка...</van-loading>
|
||||
|
||||
<template v-else>
|
||||
<van-cell-group inset>
|
||||
<van-cell
|
||||
v-for="project in projects"
|
||||
:key="project.id"
|
||||
:title="project.short_name || project.name"
|
||||
:label="project.full_name || `ID: ${project.id}`"
|
||||
clickable
|
||||
center
|
||||
@click="selectProject(project.id)"
|
||||
>
|
||||
<template #right-icon>
|
||||
<van-icon v-if="selectedProjectId === project.id" name="success" color="#1989fa" />
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
|
||||
<van-empty v-if="projects.length === 0" description="Проекты не найдены" />
|
||||
</template>
|
||||
</div>
|
||||
</van-popup>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.entity-popup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 70vh;
|
||||
max-height: 70vh;
|
||||
overflow: hidden;
|
||||
padding: 18px 0 16px;
|
||||
}
|
||||
|
||||
.entity-popup-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 0 18px 10px;
|
||||
}
|
||||
|
||||
.entity-popup-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.entity-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 36px 0;
|
||||
}
|
||||
|
||||
.entity-popup-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.entity-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
color: #323233;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
line-height: 24px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.entity-select .van-icon {
|
||||
color: #969799;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
import ContractDetailView from "./views/ContractDetailView.vue";
|
||||
import ContractsView from "./views/ContractsView.vue";
|
||||
|
||||
export const contractsRoutes = [
|
||||
{
|
||||
path: "/contracts",
|
||||
name: "contracts",
|
||||
component: ContractsView,
|
||||
meta: { title: "Контракты", requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "/contracts/:id",
|
||||
name: "contract-detail",
|
||||
component: ContractDetailView,
|
||||
meta: { title: "Детали контракта", back: true, requiresAuth: true },
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,615 @@
|
||||
<script setup lang="ts">
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { showToast } from "vant";
|
||||
import {
|
||||
contractApi,
|
||||
contractApplicationFileApi,
|
||||
} from "../../../generated/api";
|
||||
import type {
|
||||
Contract,
|
||||
ContractApplicationFile,
|
||||
ContractApplicationFileListParams,
|
||||
} from "../../../generated/models";
|
||||
import { useModelApi } from "../../../shared/composables/useModelApi";
|
||||
import PdfPreview from "../components/PdfPreview.vue";
|
||||
import DocumentApprovalTasks from "../../../shared/components/DocumentApprovalTasks.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const contractId = computed(() => Number(route.params.id));
|
||||
const activeTab = ref("info");
|
||||
|
||||
const {
|
||||
item: contract,
|
||||
loadingItem,
|
||||
error,
|
||||
retrieve: loadContract,
|
||||
} = useModelApi(contractApi, {
|
||||
retrieveErrorMessage: "Не удалось загрузить контракт",
|
||||
autoLoad: false,
|
||||
autoLoadOnFilterChange: false,
|
||||
});
|
||||
const {
|
||||
items: applicationFiles,
|
||||
loading: loadingApplicationFiles,
|
||||
error: applicationFilesError,
|
||||
load: loadApplicationFiles,
|
||||
} = useModelApi(contractApplicationFileApi, {
|
||||
defaultListParams: { ordering: "id" } as ContractApplicationFileListParams,
|
||||
loadErrorMessage: "Не удалось загрузить приложения",
|
||||
autoLoad: false,
|
||||
autoLoadOnFilterChange: false,
|
||||
});
|
||||
|
||||
const viewError = computed(() => error.value || applicationFilesError.value);
|
||||
|
||||
type DetailField = {
|
||||
title: string;
|
||||
key: keyof Contract;
|
||||
};
|
||||
|
||||
type MoneyField = DetailField & {
|
||||
money?: boolean;
|
||||
};
|
||||
|
||||
const mainFields: DetailField[] = [
|
||||
{ title: "Номер", key: "number" },
|
||||
{ title: "Дата", key: "date" },
|
||||
{ title: "Тип", key: "contract_type" },
|
||||
{ title: "Категория", key: "category" },
|
||||
{ title: "Статус", key: "status_name" },
|
||||
{ title: "НДС", key: "nds" },
|
||||
{ title: "Продукт", key: "name_of_product" },
|
||||
{ title: "Комментарий", key: "comment" },
|
||||
];
|
||||
|
||||
const moneyFields: MoneyField[] = [
|
||||
{ title: "Платеж в месяц", key: "month_pay", money: true },
|
||||
{ title: "Аванс", key: "avans_pay" },
|
||||
{ title: "Сумма", key: "amount", money: true },
|
||||
{ title: "Сумма итого", key: "amount_total", money: true },
|
||||
{ title: "Сумма итого строкой", key: "amount_total_display" },
|
||||
{ title: "Сумма по ДС", key: "amount_by_ds" },
|
||||
{ title: "Смета НДС стоимость", key: "estimate_nds_cost", money: true },
|
||||
{ title: "Смета НДС сертификат", key: "estimate_nds_cert", money: true },
|
||||
{ title: "Счета сумма", key: "bill_cost_sum", money: true },
|
||||
{ title: "Счета оплачено", key: "bill_paid_sum", money: true },
|
||||
{ title: "Доход всего", key: "income_total", money: true },
|
||||
{ title: "Задолженность", key: "arrears", money: true },
|
||||
];
|
||||
|
||||
const relationFields = computed(() => {
|
||||
const item = contract.value;
|
||||
|
||||
if (!item) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
title: "Контрагент",
|
||||
value: item.counterparty?.name ?? item.counterparty_name,
|
||||
},
|
||||
{ title: "Проект", value: item.project?.short_name ?? item.project?.name },
|
||||
{ title: "ФРЦ", value: item.frc?.name },
|
||||
{
|
||||
title: "Сотрудник",
|
||||
value: item.employee?.short_name ?? item.employee?.name,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const approvalFields = computed(() => {
|
||||
const item = contract.value;
|
||||
|
||||
if (!item) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{ title: "Статус", value: item.status_name },
|
||||
{ title: "Комментарий", value: item.comment },
|
||||
];
|
||||
});
|
||||
|
||||
const templateUrl = computed(
|
||||
() => contract.value?.category_ref?.template ?? "",
|
||||
);
|
||||
const pdfPreviewVisible = ref(false);
|
||||
const pdfPreviewFile = ref<ContractApplicationFile | null>(null);
|
||||
const pdfPreviewTitle = computed(
|
||||
() => pdfPreviewFile.value?.name || pdfPreviewFile.value?.scan_name || "PDF",
|
||||
);
|
||||
|
||||
const summaryStatus = computed(
|
||||
() => contract.value?.status_name ?? "не указано",
|
||||
);
|
||||
const summaryCategory = computed(
|
||||
() =>
|
||||
contract.value?.category_ref?.name ??
|
||||
contract.value?.category ??
|
||||
"не указано",
|
||||
);
|
||||
const summaryCounterparty = computed(
|
||||
() =>
|
||||
contract.value?.counterparty?.name ??
|
||||
contract.value?.counterparty_name ??
|
||||
"не указано",
|
||||
);
|
||||
const summaryAmount = computed(() => formatMoney(contract.value?.amount_total));
|
||||
|
||||
function displayValue(value: unknown) {
|
||||
return formatValue(value);
|
||||
}
|
||||
|
||||
function displayMoneyValue(value: unknown) {
|
||||
return formatMoney(value);
|
||||
}
|
||||
|
||||
function formatValue(value: unknown) {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return "не указано";
|
||||
}
|
||||
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function formatMoney(value: unknown) {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return "не указано";
|
||||
}
|
||||
|
||||
const amount = Number(value);
|
||||
if (!Number.isFinite(amount)) {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
return new Intl.NumberFormat("ru-RU", {
|
||||
maximumFractionDigits: 2,
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
async function openApplicationFile(scanUrl: string) {
|
||||
if (!scanUrl) {
|
||||
showToast("Файл не скачан");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await invoke("open_remote_file", {
|
||||
url: scanUrl,
|
||||
});
|
||||
} catch (err) {
|
||||
showToast(errorMessage(err, "Не удалось открыть файл"));
|
||||
}
|
||||
}
|
||||
|
||||
async function openTemplateForm() {
|
||||
if (!templateUrl.value) {
|
||||
showToast("Типовая форма не указана");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await openUrl(templateUrl.value);
|
||||
} catch (err) {
|
||||
showToast(errorMessage(err, "Не удалось открыть типовую форму"));
|
||||
}
|
||||
}
|
||||
|
||||
function isPdfFile(file: ContractApplicationFile) {
|
||||
return `${file.scan_name} ${file.name}`.toLowerCase().includes(".pdf");
|
||||
}
|
||||
|
||||
function openPdfPreview(file: ContractApplicationFile) {
|
||||
pdfPreviewFile.value = file;
|
||||
pdfPreviewVisible.value = true;
|
||||
}
|
||||
|
||||
function errorMessage(err: unknown, fallback: string) {
|
||||
if (typeof err === "object" && err && "detail" in err) {
|
||||
return String((err as { detail: unknown }).detail);
|
||||
}
|
||||
|
||||
if (err instanceof Error) {
|
||||
return err.message;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (Number.isFinite(contractId.value)) {
|
||||
loadContract(contractId.value);
|
||||
loadApplicationFiles({
|
||||
ordering: "id",
|
||||
contract_id: contractId.value,
|
||||
} as ContractApplicationFileListParams);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<van-notice-bar
|
||||
v-if="viewError"
|
||||
class="notice"
|
||||
color="#991b1b"
|
||||
background="#fee2e2"
|
||||
left-icon="warning-o"
|
||||
wrapable
|
||||
:scrollable="false"
|
||||
:text="viewError"
|
||||
/>
|
||||
|
||||
<section class="">
|
||||
<van-loading v-if="loadingItem" class="state" type="spinner"
|
||||
>Загрузка...</van-loading
|
||||
>
|
||||
|
||||
<van-empty v-else-if="!contract" description="Контракт не найден" />
|
||||
|
||||
<template v-else>
|
||||
<div class="contract-summary">
|
||||
<div class="contract-summary__title">{{ contract.name }}</div>
|
||||
<div class="contract-summary__meta">
|
||||
№ {{ contract.number }} · {{ contract.date }}
|
||||
</div>
|
||||
<div class="contract-summary__badges">
|
||||
<van-tag type="primary">{{ summaryStatus }}</van-tag>
|
||||
<van-tag plain>{{ summaryCategory }}</van-tag>
|
||||
</div>
|
||||
<van-cell-group inset>
|
||||
<van-cell>
|
||||
<template #title>
|
||||
<div class="detail-cell">
|
||||
<div class="detail-label">ЦФО</div>
|
||||
<div class="detail-value">
|
||||
{{ contract.frc?.name ?? "не указано" }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-cell>
|
||||
<template #title>
|
||||
<div class="detail-cell">
|
||||
<div class="detail-label">Объект</div>
|
||||
<div class="detail-value">
|
||||
{{
|
||||
contract.project?.short_name ??
|
||||
contract.project?.name ??
|
||||
"не указано"
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
|
||||
<van-cell>
|
||||
<template #title>
|
||||
<div class="detail-cell">
|
||||
<div class="detail-label">Предмет договора</div>
|
||||
<div class="detail-value detail-value--money">
|
||||
{{ contract.name_of_product }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-cell>
|
||||
<template #title>
|
||||
<div class="detail-cell">
|
||||
<div class="detail-label">Контрагент</div>
|
||||
<div class="detail-value">{{ summaryCounterparty }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-cell>
|
||||
<template #title>
|
||||
<div class="detail-cell">
|
||||
<div class="detail-label">Сумма итого</div>
|
||||
<div class="detail-value detail-value--money">
|
||||
{{ summaryAmount }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
</div>
|
||||
|
||||
<van-tabs v-model:active="activeTab" shrink sticky class="contract-tabs">
|
||||
<van-tab name="info" title="Информация">
|
||||
<div class="tab-panel">
|
||||
<van-cell-group>
|
||||
<van-cell v-for="field in mainFields" :key="field.key">
|
||||
<template #title>
|
||||
<div class="detail-cell">
|
||||
<div class="detail-label">{{ field.title }}</div>
|
||||
<div class="detail-value">
|
||||
{{ displayValue(contract?.[field.key]) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
|
||||
<van-cell-group title="Финансы">
|
||||
<van-cell v-for="field in moneyFields" :key="field.key">
|
||||
<template #title>
|
||||
<div class="detail-cell">
|
||||
<div class="detail-label">{{ field.title }}</div>
|
||||
<div class="detail-value detail-value--money">
|
||||
{{ displayMoneyValue(contract?.[field.key]) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
|
||||
<van-cell-group title="Связи">
|
||||
<van-cell v-for="field in relationFields" :key="field.title">
|
||||
<template #title>
|
||||
<div class="detail-cell">
|
||||
<div class="detail-label">{{ field.title }}</div>
|
||||
<div class="detail-value">
|
||||
{{ formatValue(field.value) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
|
||||
<van-cell-group title="Техническое">
|
||||
<van-cell>
|
||||
<template #title>
|
||||
<div class="detail-cell">
|
||||
<div class="detail-label">ID</div>
|
||||
<div class="detail-value">
|
||||
{{ formatValue(contract.id) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-cell>
|
||||
<template #title>
|
||||
<div class="detail-cell">
|
||||
<div class="detail-label">Код статуса</div>
|
||||
<div class="detail-value">
|
||||
{{ formatValue(contract.status) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-cell>
|
||||
<template #title>
|
||||
<div class="detail-cell">
|
||||
<div class="detail-label">URL</div>
|
||||
<div class="detail-value">
|
||||
{{ formatValue(contract.absolute_url) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
</div>
|
||||
</van-tab>
|
||||
|
||||
<van-tab name="approval" title="Согласование">
|
||||
<div class="tab-panel">
|
||||
<van-cell-group>
|
||||
<van-cell v-for="field in approvalFields" :key="field.title">
|
||||
<template #title>
|
||||
<div class="detail-cell">
|
||||
<div class="detail-label">{{ field.title }}</div>
|
||||
<div class="detail-value">
|
||||
{{ formatValue(field.value) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
|
||||
<DocumentApprovalTasks
|
||||
v-if="contract"
|
||||
:document-id="contract.id"
|
||||
filter-name="contract"
|
||||
title="Согласование договора"
|
||||
/>
|
||||
</div>
|
||||
</van-tab>
|
||||
|
||||
<van-tab name="applications" title="Приложения">
|
||||
<div class="tab-panel">
|
||||
<van-cell-group>
|
||||
<van-cell
|
||||
v-if="loadingApplicationFiles"
|
||||
title="Загрузка приложений..."
|
||||
/>
|
||||
<van-cell
|
||||
v-else-if="applicationFiles.length === 0"
|
||||
title="Приложений нет"
|
||||
/>
|
||||
<van-cell v-for="file in applicationFiles" v-else :key="file.id">
|
||||
<template #title>
|
||||
<div class="detail-cell">
|
||||
<div class="detail-label">{{ file.name }}</div>
|
||||
<div class="detail-value detail-value--muted">
|
||||
{{ file.file_type_display }} · {{ file.status_display }} ·
|
||||
{{ file.scan_name }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #value>
|
||||
<div class="file-actions">
|
||||
<van-button
|
||||
v-if="isPdfFile(file)"
|
||||
size="small"
|
||||
type="primary"
|
||||
plain
|
||||
:disabled="!file.scan_url"
|
||||
@click="openPdfPreview(file)"
|
||||
>
|
||||
Просмотр
|
||||
</van-button>
|
||||
<van-button
|
||||
size="small"
|
||||
plain
|
||||
:disabled="!file.scan_url"
|
||||
@click="openApplicationFile(file.scan_url)"
|
||||
>
|
||||
Открыть
|
||||
</van-button>
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
</div>
|
||||
</van-tab>
|
||||
|
||||
<van-tab name="works" title="Работы">
|
||||
<div class="tab-panel">
|
||||
<van-empty description="Работы по договору пока не загружены" />
|
||||
</div>
|
||||
</van-tab>
|
||||
|
||||
<van-tab name="archive" title="Архив">
|
||||
<div class="tab-panel">
|
||||
<van-empty description="Архивные материалы пока не загружены" />
|
||||
</div>
|
||||
</van-tab>
|
||||
|
||||
<van-tab name="templates" title="Типовые формы">
|
||||
<div class="tab-panel">
|
||||
<van-cell-group>
|
||||
<van-cell
|
||||
:title="contract.category_ref?.name ?? contract.category"
|
||||
:label="contract.category_ref?.name_group"
|
||||
>
|
||||
<template #title>
|
||||
<div class="detail-cell">
|
||||
<div class="detail-label">
|
||||
{{ contract.category_ref?.name ?? contract.category }}
|
||||
</div>
|
||||
<div class="detail-value detail-value--muted">
|
||||
{{ contract.category_ref?.name_group ?? "Типовая форма" }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #value>
|
||||
<van-button
|
||||
size="small"
|
||||
type="primary"
|
||||
plain
|
||||
:disabled="!templateUrl"
|
||||
@click="openTemplateForm"
|
||||
>
|
||||
Открыть
|
||||
</van-button>
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
<van-empty
|
||||
v-if="!templateUrl"
|
||||
description="Типовая форма не указана"
|
||||
/>
|
||||
</div>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
|
||||
<PdfPreview
|
||||
v-model:show="pdfPreviewVisible"
|
||||
:scan-url="pdfPreviewFile?.scan_url ?? ''"
|
||||
:title="pdfPreviewTitle"
|
||||
/>
|
||||
|
||||
<div class="detail-actions">
|
||||
<van-button block round type="primary" plain @click="router.back()"
|
||||
>Назад</van-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.contract-summary {
|
||||
margin-bottom: 12px;
|
||||
padding: 12px 0 0;
|
||||
overflow: hidden;
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
box-shadow: 0 8px 24px rgba(36, 42, 56, 0.08);
|
||||
}
|
||||
|
||||
.contract-summary__title {
|
||||
padding: 0 16px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.contract-summary__meta {
|
||||
padding: 4px 16px 0;
|
||||
color: #969799;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.contract-summary__badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
padding: 10px 16px 12px;
|
||||
}
|
||||
|
||||
.detail-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: #636b74;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: #111827;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 1.35;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.detail-value--muted {
|
||||
color: #374151;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.detail-value--money {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.contract-tabs {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.contract-tabs :deep(.van-tabs__wrap) {
|
||||
box-shadow: 0 1px 0 #ebedf0;
|
||||
}
|
||||
|
||||
.tab-panel {
|
||||
padding-top: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,381 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { showToast } from "vant";
|
||||
import { contractApi } from "../../../generated/api";
|
||||
import type { ContractListParams } from "../../../generated/models";
|
||||
import { useModelApi } from "../../../shared/composables/useModelApi";
|
||||
import ContractCategorySelect from "../components/ContractCategorySelect.vue";
|
||||
import CounterpartySelect from "../components/CounterpartySelect.vue";
|
||||
import FrcSelect from "../components/FrcSelect.vue";
|
||||
import ProjectSelect from "../components/ProjectSelect.vue";
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
type ContractFilterParams = ContractListParams & { page?: number };
|
||||
const CONTRACT_STATUS_OPTIONS = [
|
||||
{ value: "AN", text: "Аннулирован" },
|
||||
{ value: "IP", text: "В работе" },
|
||||
{ value: "OW", text: "На доработке" },
|
||||
{ value: "OS", text: "На подписи" },
|
||||
{ value: "OK", text: "Окончен" },
|
||||
{ value: "WA", text: "Проект" },
|
||||
{ value: "TE", text: "Расторгнут" },
|
||||
{ value: "AU", text: "Согласован" },
|
||||
];
|
||||
|
||||
const router = useRouter();
|
||||
const {
|
||||
items: contracts,
|
||||
filters,
|
||||
count,
|
||||
loading,
|
||||
error,
|
||||
load: loadContracts,
|
||||
} = useModelApi(contractApi, {
|
||||
defaultListParams: { ordering: "-id", page: 1 } as ContractFilterParams,
|
||||
loadErrorMessage: "Не удалось загрузить контракты",
|
||||
cleanListParams(params) {
|
||||
params.name__contains = params.name__contains?.trim() || undefined;
|
||||
params.number__contains = params.number__contains?.trim() || undefined;
|
||||
params.counterparty_id = params.counterparty_id || undefined;
|
||||
params.category_id = params.category_id || undefined;
|
||||
params.project_id = params.project_id || undefined;
|
||||
params.frc_id = params.frc_id || undefined;
|
||||
params.status = params.status || undefined;
|
||||
params.page = params.page || 1;
|
||||
},
|
||||
});
|
||||
|
||||
const contractFilters = filters as ContractFilterParams;
|
||||
|
||||
const selectedCounterpartyId = computed({
|
||||
get() {
|
||||
return filters.counterparty_id ?? 0;
|
||||
},
|
||||
set(id: number) {
|
||||
filters.counterparty_id = id || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const selectedCategoryId = computed({
|
||||
get() {
|
||||
return filters.category_id ?? 0;
|
||||
},
|
||||
set(id: number) {
|
||||
filters.category_id = id || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const selectedProjectId = computed({
|
||||
get() {
|
||||
return filters.project_id ?? 0;
|
||||
},
|
||||
set(id: number) {
|
||||
filters.project_id = id || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const selectedFrcId = computed({
|
||||
get() {
|
||||
return filters.frc_id ?? 0;
|
||||
},
|
||||
set(id: number) {
|
||||
filters.frc_id = id || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const selectedStatus = computed({
|
||||
get() {
|
||||
return filters.status ?? "";
|
||||
},
|
||||
set(status: string) {
|
||||
filters.status = status || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const hasActiveFilters = computed(() =>
|
||||
Boolean(
|
||||
filters.name__contains ||
|
||||
filters.number__contains ||
|
||||
filters.counterparty_id ||
|
||||
filters.category_id ||
|
||||
filters.project_id ||
|
||||
filters.frc_id ||
|
||||
filters.status,
|
||||
),
|
||||
);
|
||||
|
||||
const currentPage = computed({
|
||||
get() {
|
||||
return contractFilters.page ?? 1;
|
||||
},
|
||||
set(page: number) {
|
||||
contractFilters.page = page;
|
||||
},
|
||||
});
|
||||
|
||||
const syncing = ref(false);
|
||||
const showFilters = ref(false);
|
||||
|
||||
const activeFilterCount = computed(
|
||||
() =>
|
||||
[
|
||||
filters.number__contains,
|
||||
filters.counterparty_id,
|
||||
filters.category_id,
|
||||
filters.project_id,
|
||||
filters.frc_id,
|
||||
filters.status,
|
||||
].filter(Boolean).length,
|
||||
);
|
||||
|
||||
function resetFilters() {
|
||||
filters.number__contains = undefined;
|
||||
filters.counterparty_id = undefined;
|
||||
filters.category_id = undefined;
|
||||
filters.project_id = undefined;
|
||||
filters.frc_id = undefined;
|
||||
filters.status = undefined;
|
||||
contractFilters.page = 1;
|
||||
}
|
||||
|
||||
async function applyFilters() {
|
||||
contractFilters.page = 1;
|
||||
showFilters.value = false;
|
||||
await loadContracts();
|
||||
}
|
||||
|
||||
async function syncContracts() {
|
||||
syncing.value = true;
|
||||
|
||||
try {
|
||||
await loadContracts();
|
||||
showToast("Данные обновлены с сервера");
|
||||
} catch (err) {
|
||||
showToast(errorMessage(err, "Не удалось синхронизировать договоры"));
|
||||
} finally {
|
||||
syncing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function errorMessage(err: unknown, fallback: string) {
|
||||
if (typeof err === "object" && err && "detail" in err) {
|
||||
return String((err as { detail: unknown }).detail);
|
||||
}
|
||||
|
||||
if (err instanceof Error) {
|
||||
return err.message;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [
|
||||
filters.name__contains,
|
||||
filters.number__contains,
|
||||
filters.counterparty_id,
|
||||
filters.category_id,
|
||||
filters.project_id,
|
||||
filters.frc_id,
|
||||
filters.status,
|
||||
],
|
||||
() => {
|
||||
if ((contractFilters.page ?? 1) !== 1) {
|
||||
contractFilters.page = 1;
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<van-notice-bar
|
||||
v-if="error"
|
||||
class="notice"
|
||||
color="#991b1b"
|
||||
background="#fee2e2"
|
||||
left-icon="warning-o"
|
||||
wrapable
|
||||
:scrollable="false"
|
||||
:text="error"
|
||||
/>
|
||||
|
||||
<section class="contracts-page">
|
||||
<van-search
|
||||
v-model="filters.name__contains"
|
||||
placeholder="Поиск по названию"
|
||||
clearable
|
||||
/>
|
||||
|
||||
<div class="contract-actions">
|
||||
<van-badge :content="activeFilterCount || undefined">
|
||||
<van-button size="small" plain type="primary" icon="filter-o" @click="showFilters = true">
|
||||
Фильтры
|
||||
</van-button>
|
||||
</van-badge>
|
||||
<van-button size="small" plain type="primary" :loading="loading" @click="loadContracts()">
|
||||
Обновить
|
||||
</van-button>
|
||||
<van-button size="small" type="primary" :loading="syncing" @click="syncContracts">
|
||||
Обновить с сервера
|
||||
</van-button>
|
||||
</div>
|
||||
|
||||
<van-popup v-model:show="showFilters" round position="bottom" class="filters-popup">
|
||||
<div class="filters-sheet">
|
||||
<div class="filters-header">
|
||||
<h2>Фильтры</h2>
|
||||
<van-button size="small" plain type="primary" @click="resetFilters">
|
||||
Сбросить
|
||||
</van-button>
|
||||
</div>
|
||||
|
||||
<van-cell-group class="contract-filters">
|
||||
<van-field
|
||||
v-model="filters.number__contains"
|
||||
label="Номер"
|
||||
placeholder="Номер договора"
|
||||
clearable
|
||||
/>
|
||||
<van-field label="Контрагент">
|
||||
<template #input>
|
||||
<CounterpartySelect v-model="selectedCounterpartyId" />
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field label="Категория">
|
||||
<template #input>
|
||||
<ContractCategorySelect v-model="selectedCategoryId" />
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field label="Проект">
|
||||
<template #input>
|
||||
<ProjectSelect v-model="selectedProjectId" />
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field label="ФРЦ">
|
||||
<template #input>
|
||||
<FrcSelect v-model="selectedFrcId" />
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field label="Статус">
|
||||
<template #input>
|
||||
<select v-model="selectedStatus" class="native-select">
|
||||
<option value="">Все</option>
|
||||
<option
|
||||
v-for="status in CONTRACT_STATUS_OPTIONS"
|
||||
:key="status.value"
|
||||
:value="status.value"
|
||||
>
|
||||
{{ status.text }}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
||||
</van-field>
|
||||
</van-cell-group>
|
||||
|
||||
<div class="filters-actions">
|
||||
<van-button block type="primary" @click="applyFilters">
|
||||
Применить
|
||||
</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
|
||||
<van-loading
|
||||
v-if="loading && contracts.length === 0"
|
||||
class="state"
|
||||
type="spinner"
|
||||
>
|
||||
Загрузка...
|
||||
</van-loading>
|
||||
|
||||
<van-empty
|
||||
v-else-if="contracts.length === 0"
|
||||
:description="
|
||||
hasActiveFilters ? 'Договоры не найдены' : 'Контрактов пока нет'
|
||||
"
|
||||
/>
|
||||
|
||||
<template v-else>
|
||||
<van-cell-group>
|
||||
<van-cell
|
||||
v-for="contract in contracts"
|
||||
:key="contract.id"
|
||||
:title="contract.name"
|
||||
:label="`№ ${contract.number} · ${contract.counterparty?.name ?? 'не указан'}`"
|
||||
center
|
||||
is-link
|
||||
@click="router.push(`/contracts/${contract.id}`)"
|
||||
/>
|
||||
</van-cell-group>
|
||||
|
||||
<van-pagination
|
||||
v-model="currentPage"
|
||||
class="contract-pagination"
|
||||
:total-items="count"
|
||||
:items-per-page="PAGE_SIZE"
|
||||
mode="simple"
|
||||
/>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.contract-filters {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.contracts-page {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.contract-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 6px 12px 10px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.filters-popup {
|
||||
max-height: 85vh;
|
||||
}
|
||||
|
||||
.filters-sheet {
|
||||
padding: 14px 0 18px;
|
||||
}
|
||||
|
||||
.filters-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 0 16px 8px;
|
||||
}
|
||||
|
||||
.filters-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.filters-actions {
|
||||
padding: 4px 12px 0;
|
||||
}
|
||||
|
||||
.contract-pagination {
|
||||
margin: 10px 12px 0;
|
||||
}
|
||||
|
||||
.native-select {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
color: #323233;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
line-height: 24px;
|
||||
text-align: right;
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,8 @@
|
||||
import DocumentsView from "./views/DocumentsView.vue";
|
||||
|
||||
export const documentsRoute = {
|
||||
path: "/documents",
|
||||
name: "documents",
|
||||
component: DocumentsView,
|
||||
meta: { title: "Документы", requiresAuth: true },
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const tiles = [
|
||||
{
|
||||
title: "Договоры",
|
||||
icon: "orders-o",
|
||||
to: "/contracts",
|
||||
},
|
||||
{
|
||||
title: "Счета",
|
||||
icon: "description-o",
|
||||
to: "/bills",
|
||||
},
|
||||
{
|
||||
title: "Служебные записки",
|
||||
icon: "records-o",
|
||||
to: "/memos",
|
||||
},
|
||||
{
|
||||
title: "Входящие письма",
|
||||
icon: "notes-o",
|
||||
to: "/tasks?tab=incoming&doc=entry_letter",
|
||||
},
|
||||
{
|
||||
title: "Исходящие письма",
|
||||
icon: "description-o",
|
||||
to: "/tasks?tab=outgoing&doc=outgoing_letter",
|
||||
},
|
||||
] as const;
|
||||
|
||||
function openTile(path: string) {
|
||||
void router.push(path);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page documents-page">
|
||||
<div class="documents-header">
|
||||
<h1>Документы</h1>
|
||||
<p>Быстрый переход к основным разделам</p>
|
||||
</div>
|
||||
|
||||
<van-grid :border="false" :column-num="2" :gutter="12" clickable>
|
||||
<van-grid-item
|
||||
v-for="tile in tiles"
|
||||
:key="tile.title"
|
||||
class="documents-tile"
|
||||
@click="openTile(tile.to)"
|
||||
>
|
||||
<template #icon>
|
||||
<div class="documents-tile__icon">
|
||||
<van-icon :name="tile.icon" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #text>
|
||||
<div class="documents-tile__text">
|
||||
<span class="documents-tile__title">{{ tile.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.documents-page {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.documents-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.documents-header h1 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 22px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.documents-header p {
|
||||
margin: 0;
|
||||
color: var(--van-text-color-2);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.documents-tile__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
margin: 0 auto 10px;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(135deg, #eff6ff, #dbeafe);
|
||||
color: #2563eb;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.documents-tile__text {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.documents-tile__title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,140 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { memoCategoryApi } from "../../../generated/api";
|
||||
import type { MemoCategoryListParams } from "../../../generated/models";
|
||||
import { useModelApi } from "../../../shared/composables/useModelApi";
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
const selectedCategoryId = defineModel<number>({ required: true });
|
||||
const search = ref("");
|
||||
const showSelector = ref(false);
|
||||
|
||||
const {
|
||||
items: categories,
|
||||
filters,
|
||||
loading,
|
||||
} = useModelApi(memoCategoryApi, {
|
||||
defaultListParams: { ordering: "id", limit: PAGE_SIZE, offset: 0 } as MemoCategoryListParams,
|
||||
loadErrorMessage: "Не удалось загрузить категории",
|
||||
cleanListParams(params) {
|
||||
params.name__contains = params.name__contains?.trim() || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const selectedCategory = computed(() =>
|
||||
categories.value.find((category) => category.id === selectedCategoryId.value),
|
||||
);
|
||||
const selectedCategoryName = computed(() => selectedCategory.value?.name || "все");
|
||||
|
||||
function openSelector() {
|
||||
search.value = "";
|
||||
showSelector.value = true;
|
||||
}
|
||||
|
||||
function selectCategory(id: number) {
|
||||
selectedCategoryId.value = id;
|
||||
showSelector.value = false;
|
||||
}
|
||||
|
||||
watch(search, (value) => {
|
||||
filters.name__contains = value.trim();
|
||||
filters.offset = 0;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button class="entity-select" type="button" @click="openSelector">
|
||||
<span>{{ selectedCategoryName }}</span>
|
||||
<van-icon name="arrow" />
|
||||
</button>
|
||||
|
||||
<van-popup v-model:show="showSelector" round position="bottom" class="entity-popup">
|
||||
<div class="entity-popup-header">
|
||||
<h2>Выберите категорию</h2>
|
||||
<van-button size="small" type="primary" plain @click="selectCategory(0)">Все</van-button>
|
||||
</div>
|
||||
|
||||
<div class="entity-popup-body">
|
||||
<van-search v-model="search" placeholder="Поиск по категории" />
|
||||
|
||||
<van-loading v-if="loading" class="entity-state" type="spinner">Загрузка...</van-loading>
|
||||
|
||||
<template v-else>
|
||||
<van-cell-group inset>
|
||||
<van-cell
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
:title="category.name"
|
||||
:label="category.code || `ID: ${category.id}`"
|
||||
clickable
|
||||
center
|
||||
@click="selectCategory(category.id)"
|
||||
>
|
||||
<template #right-icon>
|
||||
<van-icon v-if="selectedCategoryId === category.id" name="success" color="#1989fa" />
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
|
||||
<van-empty v-if="categories.length === 0" description="Категории не найдены" />
|
||||
</template>
|
||||
</div>
|
||||
</van-popup>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.entity-popup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 70vh;
|
||||
max-height: 70vh;
|
||||
overflow: hidden;
|
||||
padding: 18px 0 16px;
|
||||
}
|
||||
|
||||
.entity-popup-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 0 18px 10px;
|
||||
}
|
||||
|
||||
.entity-popup-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.entity-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 36px 0;
|
||||
}
|
||||
|
||||
.entity-popup-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.entity-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
color: #323233;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
line-height: 24px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.entity-select .van-icon {
|
||||
color: #969799;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
import MemoDetailView from "./views/MemoDetailView.vue";
|
||||
import MemosView from "./views/MemosView.vue";
|
||||
|
||||
export const memosRoutes = [
|
||||
{
|
||||
path: "/memos",
|
||||
name: "memos",
|
||||
component: MemosView,
|
||||
meta: { title: "Служебные записки", requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "/memos/:id",
|
||||
name: "memo-detail",
|
||||
component: MemoDetailView,
|
||||
meta: { title: "Служебная записка", back: true, requiresAuth: true },
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,196 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { memoApi } from "../../../generated/api";
|
||||
import DocumentApprovalTasks from "../../../shared/components/DocumentApprovalTasks.vue";
|
||||
import { useModelApi } from "../../../shared/composables/useModelApi";
|
||||
|
||||
const route = useRoute();
|
||||
const memoId = computed(() => Number(route.params.id));
|
||||
const activeTab = ref("text");
|
||||
|
||||
const {
|
||||
item: memo,
|
||||
loadingItem,
|
||||
error,
|
||||
retrieve: loadMemo,
|
||||
} = useModelApi(memoApi, {
|
||||
retrieveErrorMessage: "Не удалось загрузить служебную записку",
|
||||
autoLoad: false,
|
||||
autoLoadOnFilterChange: false,
|
||||
});
|
||||
|
||||
function formatValue(value: unknown) {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return "не указано";
|
||||
}
|
||||
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function money(value: number | null | undefined) {
|
||||
if (typeof value !== "number") {
|
||||
return "не указано";
|
||||
}
|
||||
|
||||
return new Intl.NumberFormat("ru-RU", {
|
||||
style: "currency",
|
||||
currency: "RUB",
|
||||
maximumFractionDigits: 2,
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
function taskIds() {
|
||||
return (memo.value?.task_set ?? [])
|
||||
.map((task) => (typeof task === "object" && task ? (task as { id?: unknown }).id : undefined))
|
||||
.filter((id): id is number => typeof id === "number");
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (Number.isFinite(memoId.value)) {
|
||||
loadMemo(memoId.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<van-notice-bar
|
||||
v-if="error"
|
||||
class="notice"
|
||||
color="#991b1b"
|
||||
background="#fee2e2"
|
||||
left-icon="warning-o"
|
||||
wrapable
|
||||
:scrollable="false"
|
||||
:text="error"
|
||||
/>
|
||||
|
||||
<section class="card memo-detail-card">
|
||||
<van-loading v-if="loadingItem" class="state" type="spinner">Загрузка...</van-loading>
|
||||
|
||||
<van-empty v-else-if="!memo" description="Служебная записка не найдена" />
|
||||
|
||||
<template v-else>
|
||||
<div class="memo-summary">
|
||||
<div class="memo-summary__title">{{ memo.category?.name || `Служебная записка #${memo.id}` }}</div>
|
||||
<div class="memo-summary__meta">
|
||||
{{ memo.date }} · {{ memo.sender?.short_name || 'не указан' }} → {{ memo.recipient?.short_name || 'не указан' }}
|
||||
</div>
|
||||
<div class="memo-summary__badges">
|
||||
<van-tag :type="memo.priority === '2' ? 'danger' : 'primary'">{{ memo.priority_display }}</van-tag>
|
||||
<van-tag v-if="memo.archive" plain>Архив</van-tag>
|
||||
<van-tag v-if="memo.cancel" type="danger" plain>Отменена</van-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<van-tabs v-model:active="activeTab" shrink sticky class="memo-tabs">
|
||||
<van-tab name="text" title="Текст">
|
||||
<div class="tab-panel memo-html" v-html="memo.text" />
|
||||
</van-tab>
|
||||
|
||||
<van-tab name="info" title="Информация">
|
||||
<div class="tab-panel">
|
||||
<van-cell-group>
|
||||
<van-cell title="ID" :value="formatValue(memo.id)" />
|
||||
<van-cell title="Дата" :value="formatValue(memo.date)" />
|
||||
<van-cell title="Категория" :value="formatValue(memo.category?.name)" />
|
||||
<van-cell title="Проект" :value="formatValue(memo.project?.name)" />
|
||||
<van-cell title="Отправитель" :value="formatValue(memo.sender?.name)" />
|
||||
<van-cell title="Получатель" :value="formatValue(memo.recipient?.name)" />
|
||||
<van-cell title="Приоритет" :value="memo.priority_display || memo.priority" />
|
||||
<van-cell title="Сумма" :value="money(memo.value)" />
|
||||
<van-cell title="Дата начала" :value="formatValue(memo.date_start)" />
|
||||
<van-cell title="Дата окончания" :value="formatValue(memo.date_end)" />
|
||||
<van-cell title="Текущий шаг" :value="formatValue(memo.current_step)" />
|
||||
<van-cell title="Мое согласование" :value="formatValue(memo.my_approve)" />
|
||||
<van-cell title="Резолюция" :value="formatValue(memo.resalution)" />
|
||||
</van-cell-group>
|
||||
</div>
|
||||
</van-tab>
|
||||
|
||||
<van-tab name="tasks" title="Задачи">
|
||||
<div class="tab-panel">
|
||||
<van-empty v-if="taskIds().length === 0" description="Задачи не указаны" />
|
||||
<van-cell-group v-else>
|
||||
<van-cell v-for="taskId in taskIds()" :key="taskId" title="Задача" :value="`#${taskId}`" />
|
||||
</van-cell-group>
|
||||
</div>
|
||||
</van-tab>
|
||||
|
||||
<van-tab name="approval" title="Согласование">
|
||||
<div class="tab-panel approval-panel">
|
||||
<DocumentApprovalTasks
|
||||
:document-id="memo.id"
|
||||
filter-name="memo"
|
||||
title="Согласование служебной записки"
|
||||
/>
|
||||
</div>
|
||||
</van-tab>
|
||||
|
||||
<van-tab name="access" title="Доступ">
|
||||
<div class="tab-panel">
|
||||
<van-cell-group>
|
||||
<van-cell title="Subject" :value="formatValue(memo.subject?.join(', '))" />
|
||||
<van-cell title="Employee ACL" :value="formatValue(memo.employee_acl?.join(', '))" />
|
||||
</van-cell-group>
|
||||
</div>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.memo-detail-card {
|
||||
margin: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.memo-summary {
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #eef2ff, #f8fafc);
|
||||
}
|
||||
|
||||
.memo-summary__title {
|
||||
margin-bottom: 6px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.memo-summary__meta {
|
||||
color: var(--van-text-color-2);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.memo-summary__badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.tab-panel {
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.memo-html {
|
||||
padding: 16px;
|
||||
color: var(--van-text-color);
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.memo-html :deep(p) {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.memo-html :deep(ol),
|
||||
.memo-html :deep(ul) {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.approval-panel {
|
||||
padding: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,364 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { memoApi } from "../../../generated/api";
|
||||
import type { Memo, MemoListParams } from "../../../generated/models";
|
||||
import { useModelApi } from "../../../shared/composables/useModelApi";
|
||||
import ProjectSelect from "../../contracts/components/ProjectSelect.vue";
|
||||
import EmployeeSelect from "../../personnel/components/EmployeeSelect.vue";
|
||||
import MemoCategorySelect from "../components/MemoCategorySelect.vue";
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
const PRIORITY_OPTIONS = [
|
||||
{ text: "Все", value: "" },
|
||||
{ text: "Обычно", value: "1" },
|
||||
{ text: "Срочно", value: "2" },
|
||||
];
|
||||
|
||||
type MemoFilterParams = MemoListParams & { page?: number };
|
||||
type MemoTabName = "send" | "recive" | "by_task";
|
||||
|
||||
const router = useRouter();
|
||||
const activeTab = ref<MemoTabName>("send");
|
||||
const {
|
||||
items: memos,
|
||||
filters,
|
||||
count,
|
||||
loading,
|
||||
error,
|
||||
load: loadMemos,
|
||||
} = useModelApi(memoApi, {
|
||||
defaultListParams: { ordering: "-id", page: 1, q: "send", archive: false } as MemoFilterParams,
|
||||
loadErrorMessage: "Не удалось загрузить служебные записки",
|
||||
cleanListParams(params) {
|
||||
params.text__contains = params.text__contains?.trim() || undefined;
|
||||
params.date = params.date || undefined;
|
||||
params.priority = params.priority || undefined;
|
||||
params.recipient = params.recipient || undefined;
|
||||
params.sender = params.sender || undefined;
|
||||
params.category = params.category || undefined;
|
||||
params.project = params.project || undefined;
|
||||
params.q = params.q || activeTab.value;
|
||||
params.archive = false;
|
||||
params.page = params.page || 1;
|
||||
},
|
||||
});
|
||||
|
||||
const memoFilters = filters as MemoFilterParams;
|
||||
const showFilters = ref(false);
|
||||
|
||||
const currentPage = computed({
|
||||
get() {
|
||||
return memoFilters.page ?? 1;
|
||||
},
|
||||
set(page: number) {
|
||||
memoFilters.page = page;
|
||||
},
|
||||
});
|
||||
|
||||
const selectedSenderId = computed({
|
||||
get() {
|
||||
return filters.sender ?? 0;
|
||||
},
|
||||
set(id: number) {
|
||||
filters.sender = id || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const selectedRecipientId = computed({
|
||||
get() {
|
||||
return filters.recipient ?? 0;
|
||||
},
|
||||
set(id: number) {
|
||||
filters.recipient = id || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const selectedCategoryId = computed({
|
||||
get() {
|
||||
return filters.category ?? 0;
|
||||
},
|
||||
set(id: number) {
|
||||
filters.category = id || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const selectedProjectId = computed({
|
||||
get() {
|
||||
return filters.project ?? 0;
|
||||
},
|
||||
set(id: number) {
|
||||
filters.project = id || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const selectedPriority = computed({
|
||||
get() {
|
||||
return filters.priority ?? "";
|
||||
},
|
||||
set(priority: string) {
|
||||
filters.priority = priority || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const activeFilterCount = computed(
|
||||
() =>
|
||||
[
|
||||
filters.text__contains,
|
||||
filters.date,
|
||||
filters.priority,
|
||||
filters.recipient,
|
||||
filters.sender,
|
||||
filters.category,
|
||||
filters.project,
|
||||
].filter(Boolean).length,
|
||||
);
|
||||
|
||||
function resetFilters() {
|
||||
filters.text__contains = undefined;
|
||||
filters.date = undefined;
|
||||
filters.priority = undefined;
|
||||
filters.archive = false;
|
||||
filters.recipient = undefined;
|
||||
filters.sender = undefined;
|
||||
filters.category = undefined;
|
||||
filters.project = undefined;
|
||||
memoFilters.page = 1;
|
||||
}
|
||||
|
||||
async function applyFilters() {
|
||||
memoFilters.page = 1;
|
||||
showFilters.value = false;
|
||||
await loadMemos();
|
||||
}
|
||||
|
||||
function stripHtml(html: string) {
|
||||
return html.replace(/<[^>]*>/g, " ").replace(/ /g, " ").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function memoTitle(memo: Memo) {
|
||||
return memo.category?.name || `Служебная записка #${memo.id}`;
|
||||
}
|
||||
|
||||
function memoPreview(memo: Memo) {
|
||||
const preview = stripHtml(memo.text);
|
||||
return preview.length > 140 ? `${preview.slice(0, 140)}...` : preview;
|
||||
}
|
||||
|
||||
function openMemo(memo: Memo) {
|
||||
void router.push(`/memos/${memo.id}`);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [
|
||||
filters.text__contains,
|
||||
filters.date,
|
||||
filters.priority,
|
||||
filters.recipient,
|
||||
filters.sender,
|
||||
filters.category,
|
||||
filters.project,
|
||||
],
|
||||
() => {
|
||||
if ((memoFilters.page ?? 1) !== 1) {
|
||||
memoFilters.page = 1;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(activeTab, (tab) => {
|
||||
memoFilters.q = tab;
|
||||
memoFilters.archive = false;
|
||||
memoFilters.page = 1;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<van-notice-bar
|
||||
v-if="error"
|
||||
class="notice"
|
||||
color="#991b1b"
|
||||
background="#fee2e2"
|
||||
left-icon="warning-o"
|
||||
wrapable
|
||||
:scrollable="false"
|
||||
:text="error"
|
||||
/>
|
||||
|
||||
<section class="memos-page">
|
||||
<van-tabs v-model:active="activeTab" shrink sticky class="memo-scope-tabs">
|
||||
<van-tab name="send" title="Исходящие" />
|
||||
<van-tab name="recive" title="Входящие" />
|
||||
<van-tab name="by_task" title="На согласование" />
|
||||
</van-tabs>
|
||||
|
||||
<van-search v-model="filters.text__contains" placeholder="Поиск по тексту" clearable />
|
||||
|
||||
<div class="memo-actions">
|
||||
<van-badge :content="activeFilterCount || undefined">
|
||||
<van-button size="small" plain type="primary" icon="filter-o" @click="showFilters = true">
|
||||
Фильтры
|
||||
</van-button>
|
||||
</van-badge>
|
||||
<van-button size="small" plain type="primary" :loading="loading" @click="loadMemos()">
|
||||
Обновить
|
||||
</van-button>
|
||||
</div>
|
||||
|
||||
<van-popup v-model:show="showFilters" round position="bottom" class="filters-popup">
|
||||
<div class="filters-sheet">
|
||||
<div class="filters-header">
|
||||
<h2>Фильтры</h2>
|
||||
<van-button size="small" plain type="primary" @click="resetFilters">Сбросить</van-button>
|
||||
</div>
|
||||
|
||||
<van-cell-group>
|
||||
<van-field v-model="filters.date" label="Дата" placeholder="29.05.2026" clearable />
|
||||
<van-field label="Приоритет">
|
||||
<template #input>
|
||||
<van-dropdown-menu class="inline-dropdown">
|
||||
<van-dropdown-item v-model="selectedPriority" :options="PRIORITY_OPTIONS" />
|
||||
</van-dropdown-menu>
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field label="Категория">
|
||||
<template #input>
|
||||
<MemoCategorySelect v-model="selectedCategoryId" />
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field label="Проект">
|
||||
<template #input>
|
||||
<ProjectSelect v-model="selectedProjectId" />
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field label="Отправитель">
|
||||
<template #input>
|
||||
<EmployeeSelect v-model="selectedSenderId" />
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field label="Получатель">
|
||||
<template #input>
|
||||
<EmployeeSelect v-model="selectedRecipientId" />
|
||||
</template>
|
||||
</van-field>
|
||||
</van-cell-group>
|
||||
|
||||
<div class="filters-actions">
|
||||
<van-button block round type="primary" @click="applyFilters">Применить</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
|
||||
<van-loading v-if="loading" class="state" type="spinner">Загрузка...</van-loading>
|
||||
|
||||
<template v-else>
|
||||
<van-empty v-if="memos.length === 0" description="Служебные записки не найдены" />
|
||||
|
||||
<van-cell-group v-else inset>
|
||||
<van-cell v-for="memo in memos" :key="memo.id" clickable @click="openMemo(memo)">
|
||||
<template #title>
|
||||
<div class="memo-card">
|
||||
<div class="memo-card__top">
|
||||
<span class="memo-card__title">{{ memoTitle(memo) }}</span>
|
||||
<van-tag :type="memo.priority === '2' ? 'danger' : 'primary'" plain>
|
||||
{{ memo.priority_display }}
|
||||
</van-tag>
|
||||
</div>
|
||||
<div class="memo-card__meta">
|
||||
{{ memo.date }} · {{ memo.sender?.short_name || 'не указан' }} → {{ memo.recipient?.short_name || 'не указан' }}
|
||||
</div>
|
||||
<div class="memo-card__meta">{{ memo.project?.name || 'Проект не указан' }}</div>
|
||||
<div class="memo-card__preview">{{ memoPreview(memo) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
|
||||
<van-pagination
|
||||
v-if="count > PAGE_SIZE"
|
||||
v-model="currentPage"
|
||||
class="memo-pagination"
|
||||
:items-per-page="PAGE_SIZE"
|
||||
:total-items="count"
|
||||
force-ellipses
|
||||
@change="loadMemos()"
|
||||
/>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.memos-page {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.memo-scope-tabs {
|
||||
background: var(--van-background-2);
|
||||
}
|
||||
|
||||
.memo-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px 16px 12px;
|
||||
}
|
||||
|
||||
.filters-popup {
|
||||
max-height: 82vh;
|
||||
}
|
||||
|
||||
.filters-sheet {
|
||||
padding: 18px 0 20px;
|
||||
}
|
||||
|
||||
.filters-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px 12px;
|
||||
}
|
||||
|
||||
.filters-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.filters-actions {
|
||||
padding: 16px 16px 0;
|
||||
}
|
||||
|
||||
.inline-dropdown {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.memo-card {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.memo-card__top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.memo-card__title {
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.memo-card__meta,
|
||||
.memo-card__preview {
|
||||
color: var(--van-text-color-2);
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.memo-card__preview {
|
||||
color: var(--van-text-color);
|
||||
}
|
||||
|
||||
.memo-pagination {
|
||||
margin: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { employeeApi } from "../generated/api";
|
||||
import type { EmployeeListParams } from "../generated/models";
|
||||
import { useModelApi } from "../composables/useModelApi";
|
||||
import { employeeApi } from "../../../generated/api";
|
||||
import type { EmployeeListParams } from "../../../generated/models";
|
||||
import RemoteImage from "../../../shared/components/RemoteImage.vue";
|
||||
import { useModelApi } from "../../../shared/composables/useModelApi";
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
@@ -58,39 +59,45 @@ watch(search, (value) => {
|
||||
<van-button size="small" type="primary" plain @click="selectEmployee(0)">Не указан</van-button>
|
||||
</div>
|
||||
|
||||
<van-search v-model="search" placeholder="Поиск по имени" />
|
||||
<div class="employee-popup-body">
|
||||
<van-search v-model="search" placeholder="Поиск по имени" />
|
||||
|
||||
<van-loading v-if="loading" class="employee-state" type="spinner">Загрузка...</van-loading>
|
||||
<van-loading v-if="loading" class="employee-state" type="spinner">Загрузка...</van-loading>
|
||||
|
||||
<template v-else>
|
||||
<van-cell-group inset>
|
||||
<van-cell
|
||||
v-for="employee in employees"
|
||||
:key="employee.id"
|
||||
:title="employee.name"
|
||||
:label="`ID: ${employee.id}`"
|
||||
clickable
|
||||
center
|
||||
@click="selectEmployee(employee.id)"
|
||||
>
|
||||
<template #icon>
|
||||
<van-image class="employee-avatar" round width="36" height="36" :src="employee.avatar_small ?? ''" />
|
||||
</template>
|
||||
<template #right-icon>
|
||||
<van-icon v-if="selectedEmployeeId === employee.id" name="success" color="#1989fa" />
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
<template v-else>
|
||||
<van-cell-group inset>
|
||||
<van-cell
|
||||
v-for="employee in employees"
|
||||
:key="employee.id"
|
||||
:title="employee.name"
|
||||
:label="`ID: ${employee.id}`"
|
||||
clickable
|
||||
center
|
||||
@click="selectEmployee(employee.id)"
|
||||
>
|
||||
<template #icon>
|
||||
<RemoteImage class="employee-avatar" round width="36" height="36" :src="employee.avatar_small" />
|
||||
</template>
|
||||
<template #right-icon>
|
||||
<van-icon v-if="selectedEmployeeId === employee.id" name="success" color="#1989fa" />
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
|
||||
<van-empty v-if="employees.length === 0" description="Сотрудники не найдены" />
|
||||
</template>
|
||||
<van-empty v-if="employees.length === 0" description="Сотрудники не найдены" />
|
||||
</template>
|
||||
</div>
|
||||
</van-popup>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.employee-popup {
|
||||
min-height: 55vh;
|
||||
padding: 18px 0 28px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 70vh;
|
||||
max-height: 70vh;
|
||||
overflow: hidden;
|
||||
padding: 18px 0 16px;
|
||||
}
|
||||
|
||||
.employee-popup-header {
|
||||
@@ -116,6 +123,12 @@ watch(search, (value) => {
|
||||
padding: 36px 0;
|
||||
}
|
||||
|
||||
.employee-popup-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.employee-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -0,0 +1,8 @@
|
||||
import SettingsView from "./views/SettingsView.vue";
|
||||
|
||||
export const settingsRoute = {
|
||||
path: "/settings",
|
||||
name: "settings",
|
||||
component: SettingsView,
|
||||
meta: { title: "Настройки" },
|
||||
};
|
||||
@@ -0,0 +1,160 @@
|
||||
<script setup lang="ts">
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { showToast } from "vant";
|
||||
import { clearAuthState } from "../../../shared/auth/useAuth";
|
||||
|
||||
interface AppSettings {
|
||||
remote_base_url: string;
|
||||
auth_path: string;
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const error = ref("");
|
||||
const settings = reactive<AppSettings>({
|
||||
remote_base_url: "",
|
||||
auth_path: "/api-token-auth/",
|
||||
});
|
||||
|
||||
onMounted(loadSettings);
|
||||
|
||||
async function loadSettings() {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
|
||||
try {
|
||||
applySettings(await invoke<AppSettings>("get_app_settings"));
|
||||
} catch (err) {
|
||||
error.value = errorMessage(err, "Не удалось загрузить настройки");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
saving.value = true;
|
||||
error.value = "";
|
||||
|
||||
try {
|
||||
const saved = await invoke<AppSettings>("update_app_settings", {
|
||||
settings: {
|
||||
remote_base_url: settings.remote_base_url,
|
||||
auth_path: settings.auth_path,
|
||||
},
|
||||
});
|
||||
applySettings(saved);
|
||||
clearAuthState();
|
||||
showToast("Настройки сохранены. Войдите заново");
|
||||
router.replace("/login");
|
||||
} catch (err) {
|
||||
error.value = errorMessage(err, "Не удалось сохранить настройки");
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function resetSettings() {
|
||||
saving.value = true;
|
||||
error.value = "";
|
||||
|
||||
try {
|
||||
applySettings(await invoke<AppSettings>("reset_app_settings"));
|
||||
clearAuthState();
|
||||
showToast("Настройки сброшены. Войдите заново");
|
||||
router.replace("/login");
|
||||
} catch (err) {
|
||||
error.value = errorMessage(err, "Не удалось сбросить настройки");
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function applySettings(nextSettings: AppSettings) {
|
||||
settings.remote_base_url = nextSettings.remote_base_url;
|
||||
settings.auth_path = nextSettings.auth_path;
|
||||
}
|
||||
|
||||
function errorMessage(err: unknown, fallback: string) {
|
||||
if (typeof err === "object" && err && "detail" in err) {
|
||||
return String((err as { detail: unknown }).detail);
|
||||
}
|
||||
|
||||
if (err instanceof Error) {
|
||||
return err.message;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<van-notice-bar
|
||||
v-if="error"
|
||||
class="notice"
|
||||
color="#991b1b"
|
||||
background="#fee2e2"
|
||||
left-icon="warning-o"
|
||||
wrapable
|
||||
:scrollable="false"
|
||||
:text="error"
|
||||
/>
|
||||
|
||||
<van-form class="card" @submit="saveSettings">
|
||||
<van-loading v-if="loading" class="state" type="spinner">Загрузка...</van-loading>
|
||||
|
||||
<template v-else>
|
||||
<van-field
|
||||
v-model="settings.remote_base_url"
|
||||
name="remote_base_url"
|
||||
label="Удаленный URL"
|
||||
placeholder="http://10.0.2.2:8000"
|
||||
clearable
|
||||
:disabled="saving"
|
||||
:rules="[{ required: true, message: 'Введите URL сервера' }]"
|
||||
/>
|
||||
<van-field
|
||||
v-model="settings.auth_path"
|
||||
name="auth_path"
|
||||
label="Auth path"
|
||||
placeholder="/api-token-auth/"
|
||||
clearable
|
||||
:disabled="saving"
|
||||
:rules="[{ required: true, message: 'Введите путь авторизации' }]"
|
||||
/>
|
||||
|
||||
<div class="settings-help">
|
||||
Для Android-эмулятора сервер на хосте обычно доступен как
|
||||
<code>http://10.0.2.2:8000</code>.
|
||||
</div>
|
||||
|
||||
<div class="form-actions stacked-actions">
|
||||
<van-button block round type="primary" native-type="submit" :loading="saving">
|
||||
Сохранить
|
||||
</van-button>
|
||||
<van-button
|
||||
block
|
||||
round
|
||||
type="default"
|
||||
native-type="button"
|
||||
plain
|
||||
:disabled="saving"
|
||||
@click="resetSettings"
|
||||
>
|
||||
Сбросить по умолчанию
|
||||
</van-button>
|
||||
</div>
|
||||
</template>
|
||||
</van-form>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.settings-help {
|
||||
padding: 12px 16px 0;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
import BillsView from "./views/BillsView.vue";
|
||||
import BillDetailView from "./views/BillDetailView.vue";
|
||||
|
||||
export const supplyRoutes = [
|
||||
{
|
||||
path: "/bills",
|
||||
name: "bills",
|
||||
component: BillsView,
|
||||
meta: { title: "Счета", requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "/bills/:id",
|
||||
name: "bill-detail",
|
||||
component: BillDetailView,
|
||||
meta: { title: "Детали счета", back: true, requiresAuth: true },
|
||||
},
|
||||
];
|
||||