Initial commit: Inspector — Tax Audit Dashboard for Azerbaijan
Frappe-UI SPA (Vue 3 + Tailwind) with 5 pages: - Pulse: KPIs, charts, Health Score donut, deadlines, declarations - Inspection: tax inspections overview with drill-down tables - Tax Types: 13 AZ tax types with GL entry analysis - Accounting: Chart of Accounts browser (AZ National Plan) - Settings: configurable tax rates, thresholds, inspection params 4 DocTypes: Tax Inspection, Tax Declaration, Tax Setting, Tax Deadline Mock data layer — backend API integration planned for Phase 2 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
56fde0157e
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Root editor config file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# Common settings
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
charset = utf-8
|
||||||
|
|
||||||
|
# python, js indentation settings
|
||||||
|
[{*.py,*.js,*.vue,*.css,*.scss,*.html}]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
max_line_length = 99
|
||||||
|
|
||||||
|
# JSON files - mostly doctype schema files
|
||||||
|
[{*.json}]
|
||||||
|
insert_final_newline = false
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 1
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"node": true,
|
||||||
|
"es2022": true
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"extends": "eslint:recommended",
|
||||||
|
"rules": {
|
||||||
|
"indent": "off",
|
||||||
|
"brace-style": "off",
|
||||||
|
"no-mixed-spaces-and-tabs": "off",
|
||||||
|
"no-useless-escape": "off",
|
||||||
|
"space-unary-ops": ["error", { "words": true }],
|
||||||
|
"linebreak-style": "off",
|
||||||
|
"quotes": ["off"],
|
||||||
|
"semi": "off",
|
||||||
|
"camelcase": "off",
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"no-console": ["warn"],
|
||||||
|
"no-extra-boolean-cast": ["off"],
|
||||||
|
"no-control-regex": ["off"],
|
||||||
|
},
|
||||||
|
"root": true,
|
||||||
|
"globals": {
|
||||||
|
"frappe": true,
|
||||||
|
"Vue": true,
|
||||||
|
"SetVueGlobals": true,
|
||||||
|
"__": true,
|
||||||
|
"repl": true,
|
||||||
|
"Class": true,
|
||||||
|
"locals": true,
|
||||||
|
"cint": true,
|
||||||
|
"cstr": true,
|
||||||
|
"cur_frm": true,
|
||||||
|
"cur_dialog": true,
|
||||||
|
"cur_page": true,
|
||||||
|
"cur_list": true,
|
||||||
|
"cur_tree": true,
|
||||||
|
"msg_dialog": true,
|
||||||
|
"is_null": true,
|
||||||
|
"in_list": true,
|
||||||
|
"has_common": true,
|
||||||
|
"posthog": true,
|
||||||
|
"has_words": true,
|
||||||
|
"validate_email": true,
|
||||||
|
"open_web_template_values_editor": true,
|
||||||
|
"validate_name": true,
|
||||||
|
"validate_phone": true,
|
||||||
|
"validate_url": true,
|
||||||
|
"get_number_format": true,
|
||||||
|
"format_number": true,
|
||||||
|
"format_currency": true,
|
||||||
|
"comment_when": true,
|
||||||
|
"open_url_post": true,
|
||||||
|
"toTitle": true,
|
||||||
|
"lstrip": true,
|
||||||
|
"rstrip": true,
|
||||||
|
"strip": true,
|
||||||
|
"strip_html": true,
|
||||||
|
"replace_all": true,
|
||||||
|
"flt": true,
|
||||||
|
"precision": true,
|
||||||
|
"CREATE": true,
|
||||||
|
"AMEND": true,
|
||||||
|
"CANCEL": true,
|
||||||
|
"copy_dict": true,
|
||||||
|
"get_number_format_info": true,
|
||||||
|
"strip_number_groups": true,
|
||||||
|
"print_table": true,
|
||||||
|
"Layout": true,
|
||||||
|
"web_form_settings": true,
|
||||||
|
"$c": true,
|
||||||
|
"$a": true,
|
||||||
|
"$i": true,
|
||||||
|
"$bg": true,
|
||||||
|
"$y": true,
|
||||||
|
"$c_obj": true,
|
||||||
|
"refresh_many": true,
|
||||||
|
"refresh_field": true,
|
||||||
|
"toggle_field": true,
|
||||||
|
"get_field_obj": true,
|
||||||
|
"get_query_params": true,
|
||||||
|
"unhide_field": true,
|
||||||
|
"hide_field": true,
|
||||||
|
"set_field_options": true,
|
||||||
|
"getCookie": true,
|
||||||
|
"getCookies": true,
|
||||||
|
"get_url_arg": true,
|
||||||
|
"md5": true,
|
||||||
|
"$": true,
|
||||||
|
"jQuery": true,
|
||||||
|
"moment": true,
|
||||||
|
"hljs": true,
|
||||||
|
"Awesomplete": true,
|
||||||
|
"Sortable": true,
|
||||||
|
"gemoji": true,
|
||||||
|
"Showdown": true,
|
||||||
|
"Taggle": true,
|
||||||
|
"Gantt": true,
|
||||||
|
"Slick": true,
|
||||||
|
"Webcam": true,
|
||||||
|
"PhotoSwipe": true,
|
||||||
|
"PhotoSwipeUI_Default": true,
|
||||||
|
"io": true,
|
||||||
|
"JsBarcode": true,
|
||||||
|
"L": true,
|
||||||
|
"Chart": true,
|
||||||
|
"DataTable": true,
|
||||||
|
"Cypress": true,
|
||||||
|
"cy": true,
|
||||||
|
"it": true,
|
||||||
|
"describe": true,
|
||||||
|
"expect": true,
|
||||||
|
"context": true,
|
||||||
|
"before": true,
|
||||||
|
"beforeEach": true,
|
||||||
|
"after": true,
|
||||||
|
"qz": true,
|
||||||
|
"localforage": true,
|
||||||
|
"extend_cscript": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
.eggs
|
||||||
|
*.egg-info
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
*.egg
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Frontend build output
|
||||||
|
inspector/public/frontend/
|
||||||
|
frontend/node_modules/
|
||||||
|
frontend/yarn.lock
|
||||||
|
frontend/.vite/
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
exclude: 'node_modules|.git'
|
||||||
|
default_stages: [pre-commit]
|
||||||
|
fail_fast: false
|
||||||
|
|
||||||
|
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v6.0.0
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
files: "inspector.*"
|
||||||
|
exclude: ".*json$|.*txt$|.*csv|.*md|.*svg"
|
||||||
|
- id: check-merge-conflict
|
||||||
|
- id: check-ast
|
||||||
|
- id: check-json
|
||||||
|
- id: check-toml
|
||||||
|
- id: check-yaml
|
||||||
|
- id: debug-statements
|
||||||
|
|
||||||
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
|
rev: v0.14.10
|
||||||
|
hooks:
|
||||||
|
- id: ruff
|
||||||
|
name: "Run ruff import sorter"
|
||||||
|
args: ["--select=I", "--fix"]
|
||||||
|
|
||||||
|
- id: ruff
|
||||||
|
name: "Run ruff linter"
|
||||||
|
|
||||||
|
- id: ruff-format
|
||||||
|
name: "Run ruff formatter"
|
||||||
|
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
|
rev: v2.7.1
|
||||||
|
hooks:
|
||||||
|
- id: prettier
|
||||||
|
types_or: [javascript, vue, scss]
|
||||||
|
# Ignore any files that might contain jinja / bundles
|
||||||
|
exclude: |
|
||||||
|
(?x)^(
|
||||||
|
inspector/public/dist/.*|
|
||||||
|
.*node_modules.*|
|
||||||
|
.*boilerplate.*|
|
||||||
|
inspector/templates/includes/.*|
|
||||||
|
inspector/public/js/lib/.*
|
||||||
|
)$
|
||||||
|
|
||||||
|
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||||
|
rev: v8.44.0
|
||||||
|
hooks:
|
||||||
|
- id: eslint
|
||||||
|
types_or: [javascript]
|
||||||
|
args: ['--quiet']
|
||||||
|
# Ignore any files that might contain jinja / bundles
|
||||||
|
exclude: |
|
||||||
|
(?x)^(
|
||||||
|
inspector/public/dist/.*|
|
||||||
|
cypress/.*|
|
||||||
|
.*node_modules.*|
|
||||||
|
.*boilerplate.*|
|
||||||
|
inspector/templates/includes/.*|
|
||||||
|
inspector/public/js/lib/.*
|
||||||
|
)$
|
||||||
|
|
||||||
|
ci:
|
||||||
|
autoupdate_schedule: weekly
|
||||||
|
skip: []
|
||||||
|
submodules: false
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
### Inspector
|
||||||
|
|
||||||
|
Inspector - Tax Audit Dashboard
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
You can install this app using the [bench](https://github.com/frappe/bench) CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd $PATH_TO_YOUR_BENCH
|
||||||
|
bench get-app $URL_OF_THIS_REPO --branch main
|
||||||
|
bench install-app inspector
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
This app uses `pre-commit` for code formatting and linting. Please [install pre-commit](https://pre-commit.com/#installation) and enable it for this repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/inspector
|
||||||
|
pre-commit install
|
||||||
|
```
|
||||||
|
|
||||||
|
Pre-commit is configured to use the following tools for checking and formatting your code:
|
||||||
|
|
||||||
|
- ruff
|
||||||
|
- eslint
|
||||||
|
- prettier
|
||||||
|
- pyupgrade
|
||||||
|
|
||||||
|
### License
|
||||||
|
|
||||||
|
mit
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Inspector</title>
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Manrope:wght@300;400;500;600;700;800&family=Source+Code+Pro:wght@400;500;600;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body class="antialiased">
|
||||||
|
<div id="app"></div>
|
||||||
|
<div id="modals"></div>
|
||||||
|
<div id="popovers"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.csrf_token = "{{ csrf_token }}"
|
||||||
|
window.site_name = "{{ site_name }}"
|
||||||
|
if (!window.frappe) window.frappe = {}
|
||||||
|
frappe.boot = {{ boot }}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"name": "inspector-frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build --base=/assets/inspector/frontend/ && yarn copy-html-entry",
|
||||||
|
"copy-html-entry": "cp ../inspector/public/frontend/index.html ../inspector/www/inspector.html"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^4.4.0",
|
||||||
|
"autoprefixer": "^10.4.19",
|
||||||
|
"feather-icons": "^4.29.1",
|
||||||
|
"frappe-ui": "0.1.105",
|
||||||
|
"postcss": "^8.4.5",
|
||||||
|
"tailwindcss": "^3.4.3",
|
||||||
|
"vite": "^5.4.10",
|
||||||
|
"vue": "^3.5.12",
|
||||||
|
"vue-router": "^4.3.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<template>
|
||||||
|
<AppLayout>
|
||||||
|
<router-view />
|
||||||
|
</AppLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import AppLayout from "@/components/layout/AppLayout.vue"
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
<template>
|
||||||
|
<header
|
||||||
|
class="fixed top-0 left-0 right-0 z-[100] h-[var(--ins-header-h)] bg-white border-b border-[var(--border)] flex items-center justify-between px-6"
|
||||||
|
:style="{ paddingLeft: 'calc(var(--ins-sidebar-w) + 24px)' }"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-7">
|
||||||
|
<!-- Brand — clickable, goes to Pulse -->
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-2.5 cursor-pointer hover:opacity-80 transition-opacity"
|
||||||
|
@click="$router.push('/pulse')"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-8 h-8 rounded-lg bg-gradient-to-br from-[var(--ins-indigo)] to-purple-600 flex items-center justify-center text-white text-xs font-extrabold"
|
||||||
|
>
|
||||||
|
JI
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-base font-extrabold tracking-tight">Inspector</div>
|
||||||
|
<div
|
||||||
|
class="text-[10px] font-semibold text-[var(--ins-text-tertiary)] uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Audit Dashboard
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nav Tabs -->
|
||||||
|
<nav
|
||||||
|
class="flex gap-0.5 bg-[var(--bg-base)] rounded-lg p-[3px]"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.route"
|
||||||
|
class="px-3.5 py-[7px] rounded-md text-xs font-semibold transition-all duration-150"
|
||||||
|
:class="
|
||||||
|
currentRoute === tab.route
|
||||||
|
? 'text-[var(--ins-indigo)] bg-white shadow-sm'
|
||||||
|
: 'text-[var(--ins-text-tertiary)] hover:text-[var(--ins-text-secondary)] hover:bg-black/[0.03]'
|
||||||
|
"
|
||||||
|
@click="$router.push(tab.route)"
|
||||||
|
>
|
||||||
|
{{ tab.label }}
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<!-- Company selector -->
|
||||||
|
<select
|
||||||
|
class="px-3.5 py-1.5 pr-7 rounded-md bg-white border border-[var(--border)] font-manrope text-xs font-semibold text-[var(--ins-text-secondary)] cursor-pointer appearance-none"
|
||||||
|
style="
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%2394a3b8' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 10px center;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<option>Jey Soft MMC</option>
|
||||||
|
<option>Все компании</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- Language badge -->
|
||||||
|
<div class="flex gap-px bg-[var(--bg-base)] rounded-md p-0.5">
|
||||||
|
<button
|
||||||
|
class="px-2.5 py-1 rounded text-[10px] font-bold bg-white text-[var(--ins-indigo)] shadow-sm"
|
||||||
|
>
|
||||||
|
RU
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="px-2.5 py-1 rounded text-[10px] font-bold text-[var(--ins-text-tertiary)] cursor-not-allowed opacity-50"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
AZ
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="px-2.5 py-1 rounded text-[10px] font-bold text-[var(--ins-text-tertiary)] cursor-not-allowed opacity-50"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
EN
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from "vue"
|
||||||
|
import { useRoute } from "vue-router"
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const currentRoute = computed(() => route.path)
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ label: "Пульс", route: "/pulse" },
|
||||||
|
{ label: "Инспекция", route: "/inspection" },
|
||||||
|
{ label: "Виды налогов", route: "/tax-types" },
|
||||||
|
{ label: "Бухгалтерия", route: "/accounting" },
|
||||||
|
{ label: "Настройки", route: "/settings" },
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-[var(--bg-page)]">
|
||||||
|
<AppHeader />
|
||||||
|
<MiniSidebar />
|
||||||
|
<main
|
||||||
|
class="ml-[var(--ins-sidebar-w)] mt-[var(--ins-header-h)] px-6 py-5 max-w-content mx-auto"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import AppHeader from "./AppHeader.vue"
|
||||||
|
import MiniSidebar from "./MiniSidebar.vue"
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
<template>
|
||||||
|
<nav
|
||||||
|
class="fixed top-[var(--ins-header-h)] left-0 bottom-0 z-[90] w-[var(--ins-sidebar-w)] bg-white border-r border-[var(--border)] flex flex-col items-center pt-3.5 gap-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.route"
|
||||||
|
class="relative w-[38px] h-[38px] rounded-lg flex items-center justify-center cursor-pointer transition-all duration-150 group"
|
||||||
|
:class="
|
||||||
|
currentRoute === item.route
|
||||||
|
? 'bg-[var(--ins-indigo-dim)] text-[var(--ins-indigo)]'
|
||||||
|
: 'text-[var(--ins-text-tertiary)] hover:bg-[var(--bg-base)] hover:text-[var(--ins-text-secondary)]'
|
||||||
|
"
|
||||||
|
@click="$router.push(item.route)"
|
||||||
|
>
|
||||||
|
<div v-html="item.icon" />
|
||||||
|
<span
|
||||||
|
class="hidden group-hover:block absolute left-[46px] top-1/2 -translate-y-1/2 bg-[var(--ins-text-primary)] px-2.5 py-1 rounded-md text-[11px] font-semibold text-white whitespace-nowrap z-[999]"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from "vue"
|
||||||
|
import { useRoute } from "vue-router"
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const currentRoute = computed(() => route.path)
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
label: "Пульс",
|
||||||
|
route: "/pulse",
|
||||||
|
icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Инспекция",
|
||||||
|
route: "/inspection",
|
||||||
|
icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Налоги",
|
||||||
|
route: "/tax-types",
|
||||||
|
icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6"/></svg>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Бухгалтерия",
|
||||||
|
route: "/accounting",
|
||||||
|
icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 20h20M5 20V8l5 4V4l5 4v12"/><rect x="17" y="12" width="4" height="8"/></svg>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Настройки",
|
||||||
|
route: "/settings",
|
||||||
|
icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12.22 2h-.44a2 2 0 00-2 2v.18a2 2 0 01-1 1.73l-.43.25a2 2 0 01-2 0l-.15-.08a2 2 0 00-2.73.73l-.22.38a2 2 0 00.73 2.73l.15.1a2 2 0 011 1.72v.51a2 2 0 01-1 1.74l-.15.09a2 2 0 00-.73 2.73l.22.38a2 2 0 002.73.73l.15-.08a2 2 0 012 0l.43.25a2 2 0 011 1.73V20a2 2 0 002 2h.44a2 2 0 002-2v-.18a2 2 0 011-1.73l.43-.25a2 2 0 012 0l.15.08a2 2 0 002.73-.73l.22-.39a2 2 0 00-.73-2.73l-.15-.08a2 2 0 01-1-1.74v-.5a2 2 0 011-1.74l.15-.09a2 2 0 00.73-2.73l-.22-.38a2 2 0 00-2.73-.73l-.15.08a2 2 0 01-2 0l-.43-.25a2 2 0 01-1-1.73V4a2 2 0 00-2-2z"/><circle cx="12" cy="12" r="3"/></svg>',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div
|
||||||
|
v-for="(item, i) in items"
|
||||||
|
:key="i"
|
||||||
|
class="flex gap-3 py-2.5 relative"
|
||||||
|
>
|
||||||
|
<!-- Connecting line -->
|
||||||
|
<div
|
||||||
|
v-if="i < items.length - 1"
|
||||||
|
class="absolute left-[15px] top-[36px] bottom-[-2px] w-[2px] bg-[var(--border)]"
|
||||||
|
/>
|
||||||
|
<!-- Dot -->
|
||||||
|
<div
|
||||||
|
class="w-8 h-8 rounded-full flex items-center justify-center text-sm flex-shrink-0 z-[1] border-2"
|
||||||
|
:class="dotClass(item.urgency)"
|
||||||
|
>
|
||||||
|
{{ item.icon }}
|
||||||
|
</div>
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="text-[13px] font-bold">{{ item.title }}</div>
|
||||||
|
<div class="text-[11px] font-semibold text-[var(--ins-text-tertiary)] font-data">
|
||||||
|
{{ item.date }}
|
||||||
|
<span
|
||||||
|
class="text-[10px] font-bold px-[7px] py-[2px] rounded-[10px] ml-1.5"
|
||||||
|
:class="daysClass(item.urgency)"
|
||||||
|
>
|
||||||
|
{{ item.days <= 1 ? item.days + ' день!' : item.days + ' дней' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
function dotClass(urgency) {
|
||||||
|
if (urgency === "urgent") return "border-red-500 bg-red-50 animate-[timelinePulse_2s_infinite]"
|
||||||
|
if (urgency === "soon") return "border-amber-500 bg-amber-50"
|
||||||
|
return "border-emerald-500 bg-emerald-50"
|
||||||
|
}
|
||||||
|
|
||||||
|
function daysClass(urgency) {
|
||||||
|
if (urgency === "urgent") return "bg-[var(--ins-negative-bg)] text-[var(--ins-negative)]"
|
||||||
|
if (urgency === "soon") return "bg-[var(--ins-warning-bg)] text-[var(--ins-warning)]"
|
||||||
|
return "bg-[var(--ins-positive-bg)] text-[var(--ins-positive)]"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-for="item in items" :key="item.name" class="mb-2.5">
|
||||||
|
<div class="flex justify-between text-[11px] font-semibold mb-1">
|
||||||
|
<span class="text-[var(--ins-text-secondary)]">{{ item.name }}</span>
|
||||||
|
<span class="text-[var(--ins-text-tertiary)] font-data">
|
||||||
|
{{ item.filed }}/{{ item.total }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="h-2 bg-[var(--bg-base)] rounded overflow-hidden flex"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="h-full transition-all duration-500"
|
||||||
|
:style="{ width: filedPct(item) + '%', background: 'var(--ins-positive)' }"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="pendingPct(item) > 0"
|
||||||
|
class="h-full transition-all duration-500"
|
||||||
|
:style="{ width: pendingPct(item) + '%', background: 'var(--ins-warning)' }"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="latePct(item) > 0"
|
||||||
|
class="h-full transition-all duration-500"
|
||||||
|
:style="{ width: latePct(item) + '%', background: 'var(--ins-negative)' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2.5 flex gap-3 text-[10px] font-bold">
|
||||||
|
<span class="text-[var(--ins-positive)]">● Сдано</span>
|
||||||
|
<span class="text-[var(--ins-warning)]">● Ожидает</span>
|
||||||
|
<span class="text-[var(--ins-negative)]">● Просрочено</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
// [{ name, filed, total, late }]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
function filedPct(item) {
|
||||||
|
return (item.filed / item.total) * 100
|
||||||
|
}
|
||||||
|
function latePct(item) {
|
||||||
|
return ((item.late || 0) / item.total) * 100
|
||||||
|
}
|
||||||
|
function pendingPct(item) {
|
||||||
|
return ((item.total - item.filed - (item.late || 0)) / item.total) * 100
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center justify-center py-5">
|
||||||
|
<div
|
||||||
|
class="relative w-[180px] h-[180px]"
|
||||||
|
:class="{ spinning: spinning }"
|
||||||
|
:style="spinning ? '' : 'animation: donutPulse 3s ease-in-out infinite, donutBreathe 4s ease-in-out infinite'"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 180 180" class="w-[180px] h-[180px]" :style="spinning ? 'animation: ringSpin 1.5s linear infinite' : 'transform: rotate(-90deg)'">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="healthGrad" x1="0" y1="0" x2="1" y2="0">
|
||||||
|
<stop offset="0%" stop-color="var(--ins-negative)" />
|
||||||
|
<stop offset="40%" stop-color="var(--ins-warning)" />
|
||||||
|
<stop offset="100%" stop-color="var(--ins-positive)" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<circle
|
||||||
|
cx="90" cy="90" r="76"
|
||||||
|
fill="none"
|
||||||
|
stroke="var(--bg-base)"
|
||||||
|
stroke-width="14"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="90" cy="90" r="76"
|
||||||
|
fill="none"
|
||||||
|
stroke="var(--ins-indigo)"
|
||||||
|
stroke-width="18"
|
||||||
|
opacity="0.1"
|
||||||
|
:stroke-dasharray="circumference"
|
||||||
|
:stroke-dashoffset="glowOffset"
|
||||||
|
style="filter: blur(4px)"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="90" cy="90" r="76"
|
||||||
|
fill="none"
|
||||||
|
:stroke="spinning ? 'var(--ins-indigo)' : 'url(#healthGrad)'"
|
||||||
|
stroke-width="14"
|
||||||
|
stroke-linecap="round"
|
||||||
|
:stroke-dasharray="circumference"
|
||||||
|
:stroke-dashoffset="spinning ? 380 : fillOffset"
|
||||||
|
:style="spinning ? '' : 'transition: stroke-dashoffset 1s cubic-bezier(.22,1,.36,1)'"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div class="absolute inset-0 flex flex-col items-center justify-center">
|
||||||
|
<div class="text-5xl font-extrabold tracking-tighter leading-none text-[var(--ins-text-primary)]">
|
||||||
|
{{ score }}
|
||||||
|
</div>
|
||||||
|
<div class="text-[10px] font-bold text-[var(--ins-text-tertiary)] uppercase tracking-widest mt-1">
|
||||||
|
Health Score
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="mt-2.5 px-3.5 py-1 rounded-full text-xs font-bold cursor-pointer transition-all duration-150 hover:scale-105"
|
||||||
|
:class="
|
||||||
|
score >= 80
|
||||||
|
? 'bg-[var(--ins-positive-bg)] text-[var(--ins-positive)]'
|
||||||
|
: score >= 50
|
||||||
|
? 'bg-[var(--ins-warning-bg)] text-[var(--ins-warning)]'
|
||||||
|
: 'bg-[var(--ins-negative-bg)] text-[var(--ins-negative)]'
|
||||||
|
"
|
||||||
|
@click="$emit('attention')"
|
||||||
|
>
|
||||||
|
{{ score >= 80 ? '✓ Хорошо' : score >= 50 ? '⚠ Требует внимания' : '✕ Критично' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from "vue"
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
score: { type: Number, default: 75 },
|
||||||
|
spinning: { type: Boolean, default: false },
|
||||||
|
})
|
||||||
|
|
||||||
|
defineEmits(["attention"])
|
||||||
|
|
||||||
|
const circumference = 2 * Math.PI * 76 // ~478
|
||||||
|
const fillOffset = computed(() => circumference * (1 - props.score / 100))
|
||||||
|
const glowOffset = computed(() => circumference * (1 - props.score / 100))
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-3.5 px-5 py-3.5 glass rounded-xl flex-1 cursor-pointer select-none transition-all duration-300 hover:translate-y-[-2px] hover:shadow-lg active:scale-[0.97] active:duration-[60ms]"
|
||||||
|
:class="{ 'pointer-events-none': skeleton }"
|
||||||
|
@click="$emit('click')"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-[44px] h-[44px] rounded-full flex items-center justify-center text-base relative flex-shrink-0"
|
||||||
|
:class="iconClass"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="absolute inset-[-3px] rounded-full border-2 opacity-20"
|
||||||
|
:class="ringClass"
|
||||||
|
/>
|
||||||
|
<span class="text-xs font-extrabold">{{ up ? '↑' : '↓' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div
|
||||||
|
class="text-2xl font-extrabold tracking-tight leading-none text-[var(--ins-text-primary)]"
|
||||||
|
:class="{ 'animate-shimmer text-transparent rounded': skeleton }"
|
||||||
|
:style="valueStyle"
|
||||||
|
v-html="value"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="text-[10px] font-semibold text-[var(--ins-text-tertiary)] mt-[3px] flex items-center gap-1.5"
|
||||||
|
:class="{ 'animate-shimmer text-transparent rounded': skeleton }"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
<span
|
||||||
|
v-if="change && !skeleton"
|
||||||
|
class="text-[10px] font-bold px-1.5 py-[1px] rounded-[10px]"
|
||||||
|
:class="
|
||||||
|
up
|
||||||
|
? 'bg-[var(--ins-positive-bg)] text-[var(--ins-positive)]'
|
||||||
|
: 'bg-[var(--ins-negative-bg)] text-[var(--ins-negative)]'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ change }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from "vue"
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
label: String,
|
||||||
|
value: [String, Number],
|
||||||
|
change: String,
|
||||||
|
up: { type: Boolean, default: true },
|
||||||
|
color: { type: String, default: "green" },
|
||||||
|
skeleton: { type: Boolean, default: false },
|
||||||
|
valueStyle: { type: String, default: "" },
|
||||||
|
})
|
||||||
|
|
||||||
|
defineEmits(["click"])
|
||||||
|
|
||||||
|
const iconClass = computed(() => ({
|
||||||
|
"bg-emerald-500/[0.08] text-emerald-600": props.color === "green",
|
||||||
|
"bg-amber-500/[0.08] text-amber-600": props.color === "yellow",
|
||||||
|
"bg-blue-500/[0.08] text-blue-600": props.color === "blue",
|
||||||
|
"bg-red-500/[0.08] text-red-600": props.color === "red",
|
||||||
|
"bg-indigo-500/[0.08] text-indigo-600": props.color === "indigo",
|
||||||
|
}))
|
||||||
|
|
||||||
|
const ringClass = computed(() => ({
|
||||||
|
"border-emerald-600": props.color === "green",
|
||||||
|
"border-amber-600": props.color === "yellow",
|
||||||
|
"border-blue-600": props.color === "blue",
|
||||||
|
"border-red-600": props.color === "red",
|
||||||
|
"border-indigo-600": props.color === "indigo",
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-for="bar in bars"
|
||||||
|
:key="bar.name"
|
||||||
|
class="flex items-center gap-2 mb-2"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="text-[11px] font-semibold text-[var(--ins-text-secondary)] min-w-[72px] text-right"
|
||||||
|
>
|
||||||
|
{{ bar.name }}
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 h-[22px] bg-[var(--bg-base)] rounded overflow-hidden">
|
||||||
|
<div
|
||||||
|
class="h-full rounded flex items-center pl-2 text-[10px] font-bold text-white transition-all duration-700"
|
||||||
|
:style="{ width: bar.widthPct + '%', background: bar.color }"
|
||||||
|
>
|
||||||
|
₼{{ bar.value }}M
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-[11px] font-bold text-[var(--ins-text-tertiary)] font-data min-w-[44px]"
|
||||||
|
>
|
||||||
|
{{ bar.pct }}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from "vue"
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
bars: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
// [{ name, value, pct, color }]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,162 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="visible" class="mt-2" :class="{ 'detail-leaving': isLeaving }">
|
||||||
|
<!-- Back bar -->
|
||||||
|
<div class="detail-stage" style="animation-delay: 0s">
|
||||||
|
<div class="flex items-center gap-3 mb-2">
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-1.5 px-4 py-2 rounded-lg border border-[var(--border)] bg-white cursor-pointer font-manrope text-[13px] font-bold text-[var(--ins-indigo)] transition-all duration-150 hover:bg-[var(--ins-indigo-dim)] hover:border-[var(--ins-indigo-border)] active:scale-[0.97]"
|
||||||
|
style="box-shadow: var(--shadow-sm)"
|
||||||
|
@click="handleClose"
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
||||||
|
<path d="M19 12H5M12 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
Назад
|
||||||
|
</button>
|
||||||
|
<span class="text-sm font-extrabold text-[var(--ins-text-primary)]">{{ title }}</span>
|
||||||
|
<span
|
||||||
|
v-if="badge"
|
||||||
|
class="text-[11px] font-bold px-2.5 py-[3px] rounded-full bg-[var(--ins-indigo-light)] text-[var(--ins-indigo)]"
|
||||||
|
>{{ badge }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Detail table -->
|
||||||
|
<div
|
||||||
|
class="bg-white border border-[var(--border)] rounded-xl overflow-hidden detail-stage"
|
||||||
|
style="box-shadow: var(--shadow-lg); animation-delay: 0.1s"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between px-5 py-4 border-b border-[var(--border)] detail-stage"
|
||||||
|
style="animation-delay: 0.25s"
|
||||||
|
>
|
||||||
|
<div class="text-[15px] font-extrabold text-[var(--ins-text-primary)]">{{ title }}</div>
|
||||||
|
<button
|
||||||
|
class="w-8 h-8 rounded-md border border-[var(--border)] bg-white cursor-pointer flex items-center justify-center text-base text-[var(--ins-text-tertiary)] transition-all duration-150 hover:bg-[var(--bg-base)] hover:text-[var(--ins-text-primary)]"
|
||||||
|
@click="handleClose"
|
||||||
|
>✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-5 py-4">
|
||||||
|
<table class="w-full border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
v-for="(col, i) in columns" :key="i"
|
||||||
|
class="text-left text-[11px] font-bold uppercase tracking-wider text-[var(--ins-text-tertiary)] px-3.5 py-3 bg-[var(--bg-page)] border-b-2 border-[var(--border)]"
|
||||||
|
>{{ col }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="(row, ri) in rows" :key="ri"
|
||||||
|
class="hover:bg-[rgba(79,70,229,0.03)] row-stagger"
|
||||||
|
:style="{ animationDelay: `${0.35 + Math.floor(ri / 2) * 0.08}s` }"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
v-for="(cell, ci) in row" :key="ci"
|
||||||
|
class="text-[13px] font-medium text-[var(--ins-text-secondary)] px-3.5 py-[11px] border-b border-[var(--border-light)]"
|
||||||
|
v-html="formatCell(cell)"
|
||||||
|
/>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="summary"
|
||||||
|
class="px-5 py-3.5 bg-[var(--bg-base)] border-t border-[var(--border)] text-xs font-semibold text-[var(--ins-text-secondary)] detail-stage"
|
||||||
|
style="animation-delay: 0.7s"
|
||||||
|
v-html="summary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from "vue"
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
open: { type: Boolean, default: false },
|
||||||
|
title: { type: String, default: "" },
|
||||||
|
badge: { type: String, default: "" },
|
||||||
|
columns: { type: Array, default: () => [] },
|
||||||
|
rows: { type: Array, default: () => [] },
|
||||||
|
summary: { type: String, default: "" },
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(["close"])
|
||||||
|
const visible = ref(false)
|
||||||
|
const isLeaving = ref(false)
|
||||||
|
|
||||||
|
// Sync with open prop
|
||||||
|
watch(() => props.open, (val) => {
|
||||||
|
if (val) {
|
||||||
|
visible.value = true
|
||||||
|
isLeaving.value = false
|
||||||
|
}
|
||||||
|
// Close is handled by handleClose, not by prop change
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
// Start leave animation
|
||||||
|
isLeaving.value = true
|
||||||
|
// After animation completes, hide and emit
|
||||||
|
setTimeout(() => {
|
||||||
|
visible.value = false
|
||||||
|
isLeaving.value = false
|
||||||
|
emit("close")
|
||||||
|
}, 450)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCell(val) {
|
||||||
|
const s = String(val)
|
||||||
|
if (/^\d{3}(\.\d+)?$/.test(s)) return `<span class="bg-[var(--bg-base)] px-2 py-[2px] rounded font-data text-[11px] font-semibold text-[var(--ins-indigo)] whitespace-nowrap">${s}</span>`
|
||||||
|
if (s === "✓") return `<span class="inline-flex items-center px-2.5 py-[3px] rounded-full text-[10px] font-bold bg-[var(--ins-positive-bg)] text-[var(--ins-positive)]">✓</span>`
|
||||||
|
if (s === "Открыта") return `<span class="inline-flex items-center px-2.5 py-[3px] rounded-full text-[10px] font-bold bg-[var(--ins-negative-bg)] text-[var(--ins-negative)]">${s}</span>`
|
||||||
|
if (s === "Закрыта") return `<span class="inline-flex items-center px-2.5 py-[3px] rounded-full text-[10px] font-bold bg-[var(--ins-positive-bg)] text-[var(--ins-positive)]">${s}</span>`
|
||||||
|
if (["В работе", "Ожидание", "Ожидает", "Ожид."].includes(s)) return `<span class="inline-flex items-center px-2.5 py-[3px] rounded-full text-[10px] font-bold bg-[var(--ins-warning-bg)] text-[var(--ins-warning)]">${s}</span>`
|
||||||
|
if (s === "СРОЧНО") return `<span class="inline-flex items-center px-2.5 py-[3px] rounded-full text-[10px] font-bold bg-[var(--ins-negative-bg)] text-[var(--ins-negative)]">${s}</span>`
|
||||||
|
if (s.startsWith("Риск ")) return `<span class="inline-flex items-center px-2.5 py-[3px] rounded-full text-[10px] font-bold bg-[var(--ins-negative-bg)] text-[var(--ins-negative)]">${s}</span>`
|
||||||
|
if (["Контроль", "Шкала", "Дедлайн"].includes(s)) return `<span class="inline-flex items-center px-2.5 py-[3px] rounded-full text-[10px] font-bold bg-[var(--ins-warning-bg)] text-[var(--ins-warning)]">${s}</span>`
|
||||||
|
if (["Критический", "Высокий"].includes(s)) return `<span class="inline-flex items-center px-2.5 py-[3px] rounded-full text-[10px] font-bold bg-[var(--ins-negative-bg)] text-[var(--ins-negative)]">${s}</span>`
|
||||||
|
if (s === "Средний") return `<span class="inline-flex items-center px-2.5 py-[3px] rounded-full text-[10px] font-bold bg-[var(--ins-warning-bg)] text-[var(--ins-warning)]">${s}</span>`
|
||||||
|
if (s === "Низкий") return `<span class="inline-flex items-center px-2.5 py-[3px] rounded-full text-[10px] font-bold bg-[var(--ins-positive-bg)] text-[var(--ins-positive)]">${s}</span>`
|
||||||
|
if (/^₼/.test(s)) return `<span class="font-data font-bold text-[13px] text-[var(--ins-text-primary)]">${s}</span>`
|
||||||
|
if (s === "▲") return `<span class="inline-flex items-center px-2.5 py-[3px] rounded-full text-[10px] font-bold bg-[var(--ins-warning-bg)] text-[var(--ins-warning)]">▲</span>`
|
||||||
|
if (s === "→") return `<span class="inline-flex items-center px-2.5 py-[3px] rounded-full text-[10px] font-bold bg-[var(--ins-positive-bg)] text-[var(--ins-positive)]">→</span>`
|
||||||
|
if (/^\d{1,3}$/.test(s) && Number(s) > 0) {
|
||||||
|
const n = Number(s)
|
||||||
|
if (n > 80) return `<span class="font-data font-bold text-[var(--ins-negative)]">${s}</span>`
|
||||||
|
if (n > 50) return `<span class="font-data font-bold text-[var(--ins-warning)]">${s}</span>`
|
||||||
|
return `<span class="font-data font-bold text-[var(--ins-positive)]">${s}</span>`
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.detail-stage {
|
||||||
|
animation: detailEnter 0.5s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||||
|
}
|
||||||
|
@keyframes detailEnter {
|
||||||
|
from { opacity: 0; transform: translateY(16px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-leaving {
|
||||||
|
animation: detailLeave 0.45s cubic-bezier(0.4, 0, 0.2, 1) both;
|
||||||
|
}
|
||||||
|
@keyframes detailLeave {
|
||||||
|
from { opacity: 1; transform: translateY(0) scale(1); }
|
||||||
|
to { opacity: 0; transform: translateY(14px) scale(0.98); }
|
||||||
|
}
|
||||||
|
.detail-leaving .detail-stage {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
.detail-leaving .row-stagger {
|
||||||
|
animation: none;
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-2 flex-wrap mb-5 px-4 py-2.5 bg-white border border-[var(--border)] rounded-lg"
|
||||||
|
style="box-shadow: var(--shadow-sm)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-[10px] font-bold uppercase tracking-widest text-[var(--ins-text-tertiary)] mr-1"
|
||||||
|
>
|
||||||
|
{{ title }}
|
||||||
|
</span>
|
||||||
|
<template v-for="(group, gi) in groups" :key="gi">
|
||||||
|
<div
|
||||||
|
v-if="gi > 0 && group.separator"
|
||||||
|
class="w-px h-[22px] bg-[var(--border)] mx-1.5"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="group.label"
|
||||||
|
class="text-[10px] font-bold uppercase tracking-widest text-[var(--ins-text-tertiary)] mr-1"
|
||||||
|
>
|
||||||
|
{{ group.label }}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
v-for="chip in group.chips"
|
||||||
|
:key="chip.key"
|
||||||
|
class="px-3 py-[5px] rounded-full cursor-pointer text-xs font-semibold border transition-all duration-150 select-none"
|
||||||
|
:class="
|
||||||
|
isActive(chip.key)
|
||||||
|
? 'bg-[var(--ins-indigo-light)] border-[var(--ins-indigo-border)] text-[var(--ins-indigo)]'
|
||||||
|
: 'bg-white border-[var(--border)] text-[var(--ins-text-tertiary)] hover:border-[var(--ins-indigo-border)] hover:text-[var(--ins-text-secondary)]'
|
||||||
|
"
|
||||||
|
@click="toggle(chip.key)"
|
||||||
|
>
|
||||||
|
{{ chip.label }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
title: { type: String, default: "" },
|
||||||
|
groups: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
// [{ label?, separator?, chips: [{ key, label }] }]
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
// { chipKey: true/false }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:modelValue"])
|
||||||
|
|
||||||
|
function isActive(key) {
|
||||||
|
return props.modelValue[key] !== false
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(key) {
|
||||||
|
const newVal = { ...props.modelValue }
|
||||||
|
newVal[key] = !isActive(key)
|
||||||
|
emit("update:modelValue", newVal)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="relative bg-white border rounded-xl px-4 py-[18px] cursor-pointer select-none overflow-hidden transition-all duration-300"
|
||||||
|
:class="[
|
||||||
|
status === 'critical'
|
||||||
|
? 'border-red-500/30 animate-pulse-danger'
|
||||||
|
: status === 'warning'
|
||||||
|
? 'border-amber-500/30 animate-pulse-warning'
|
||||||
|
: 'border-[var(--border)]',
|
||||||
|
'hover:translate-y-[-3px] hover:scale-[1.008] hover:shadow-lg',
|
||||||
|
'active:translate-y-[1px] active:scale-[0.97] active:shadow-[var(--shadow-press)] active:duration-[60ms]',
|
||||||
|
]"
|
||||||
|
style="box-shadow: var(--shadow-sm)"
|
||||||
|
@click="$emit('click')"
|
||||||
|
>
|
||||||
|
<!-- Top accent line -->
|
||||||
|
<div
|
||||||
|
class="absolute top-0 left-0 right-0 h-[3px] rounded-t-xl transition-opacity duration-200"
|
||||||
|
:class="[
|
||||||
|
status === 'critical'
|
||||||
|
? 'opacity-100 bg-gradient-to-r from-red-600 to-red-400'
|
||||||
|
: status === 'warning'
|
||||||
|
? 'opacity-100 bg-gradient-to-r from-amber-600 to-amber-400'
|
||||||
|
: 'opacity-0 group-hover:opacity-100 bg-gradient-to-r from-[var(--ins-indigo)] to-purple-600',
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span
|
||||||
|
class="text-[11px] font-bold text-[var(--ins-text-tertiary)] tracking-wider uppercase"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="change"
|
||||||
|
class="text-[11px] font-bold px-2 py-[3px] rounded-full font-data"
|
||||||
|
:class="
|
||||||
|
changeUp
|
||||||
|
? 'bg-[var(--ins-positive-bg)] text-[var(--ins-positive)]'
|
||||||
|
: 'bg-[var(--ins-negative-bg)] text-[var(--ins-negative)]'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ change }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="text-[26px] font-extrabold tracking-tight leading-none"
|
||||||
|
:class="status === 'critical' ? 'text-[var(--ins-negative)]' : 'text-[var(--ins-text-primary)]'"
|
||||||
|
:style="smallValue ? 'font-size: 18px' : ''"
|
||||||
|
>
|
||||||
|
{{ value }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="subtitle"
|
||||||
|
class="text-xs text-[var(--ins-text-tertiary)] mt-1.5 font-medium"
|
||||||
|
>
|
||||||
|
{{ subtitle }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
label: String,
|
||||||
|
value: [String, Number],
|
||||||
|
subtitle: String,
|
||||||
|
change: String,
|
||||||
|
changeUp: { type: Boolean, default: true },
|
||||||
|
status: { type: String, default: "normal" },
|
||||||
|
smallValue: { type: Boolean, default: false },
|
||||||
|
})
|
||||||
|
|
||||||
|
defineEmits(["click"])
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,466 @@
|
||||||
|
export const D = {
|
||||||
|
pulse: {
|
||||||
|
revenue: {
|
||||||
|
title: "Выручка — помесячно",
|
||||||
|
head: ["Месяц", "Выручка", "Расходы", "Прибыль", "Налоги"],
|
||||||
|
rows: [
|
||||||
|
["Январь", "₼320K", "₼245K", "₼75K", "₼48K"],
|
||||||
|
["Февраль", "₼355K", "₼260K", "₼95K", "₼52K"],
|
||||||
|
["Март", "₼410K", "₼280K", "₼130K", "₼61K"],
|
||||||
|
["Апрель", "₼380K", "₼270K", "₼110K", "₼55K"],
|
||||||
|
["Май", "₼365K", "₼255K", "₼110K", "₼54K"],
|
||||||
|
["Июнь", "₼340K", "₼248K", "₼92K", "₼49K"],
|
||||||
|
],
|
||||||
|
sum: "Итого за H1: <b>₼2.17M</b> · Средн.: <b>₼362K/мес.</b>",
|
||||||
|
},
|
||||||
|
taxload: {
|
||||||
|
title: "Налоговая нагрузка по видам",
|
||||||
|
head: ["Налог", "Сумма", "% от выручки", "Тренд"],
|
||||||
|
rows: [
|
||||||
|
["ƏDV", "₼285K", "6.8%", "▲"],
|
||||||
|
["Gəlir", "₼171K", "4.1%", "→"],
|
||||||
|
["DSMF+İTS", "₼162K", "3.9%", "→"],
|
||||||
|
["Mənfəət", "₼84K", "2.0%", "▲"],
|
||||||
|
["Прочие", "₼71K", "1.7%", "→"],
|
||||||
|
],
|
||||||
|
sum: "Общая нагрузка: <b>18.4%</b> от выручки",
|
||||||
|
},
|
||||||
|
employees: {
|
||||||
|
title: "Список сотрудников",
|
||||||
|
head: ["ФИО", "Должность", "Оклад", "DSMF", "Gəlir"],
|
||||||
|
rows: [
|
||||||
|
["Мамедов Р.А.", "Директор", "₼12,000", "₼3,000", "₼560"],
|
||||||
|
["Гулиев Э.Ф.", "Гл.бухгалтер", "₼26,000", "₼6,500", "₼2,770"],
|
||||||
|
["Сулейманов Н.И.", "Разработчик", "₼28,500", "₼7,125", "₼3,395"],
|
||||||
|
["Алиева С.А.", "HR менеджер", "₼9,500", "₼2,375", "₼210"],
|
||||||
|
["Гасанов Т.К.", "Менеджер", "₼8,200", "₼2,050", "₼28"],
|
||||||
|
["Исмаилова Л.Р.", "Ассистент", "₼7,800", "₼1,950", "₼0"],
|
||||||
|
["Прочие (18 чел.)", "—", "₼94,400", "₼23,600", "₼7,317"],
|
||||||
|
],
|
||||||
|
sum: "Всего: <b>24 чел.</b> · ФОТ: <b>₼186,400</b> · DSMF: <b>₼46,600</b>",
|
||||||
|
},
|
||||||
|
declarations: {
|
||||||
|
title: "Статус всех деклараций 2025",
|
||||||
|
head: ["Налог", "Периодичность", "Сдано", "Ожидает", "Просрочено"],
|
||||||
|
rows: [
|
||||||
|
["ƏDV", "Ежемесячно", "12", "0", "0"],
|
||||||
|
["Gəlir vergisi", "Ежемесячно", "12", "0", "0"],
|
||||||
|
["DSMF / İTS", "Ежемесячно", "11", "1", "0"],
|
||||||
|
["Mənfəət", "Ежеквартально", "3", "1", "0"],
|
||||||
|
["Mənbədə tutulan", "Ежемесячно", "10", "0", "2"],
|
||||||
|
["Əmlak", "Ежеквартально", "4", "0", "0"],
|
||||||
|
["Sadələşdirilmiş", "Ежеквартально", "4", "0", "0"],
|
||||||
|
],
|
||||||
|
sum: "Сдано: <b>56</b> · Ожидает: <b>2</b> · Просрочено: <b>2</b>",
|
||||||
|
},
|
||||||
|
inspections: {
|
||||||
|
title: "Открытые проверки",
|
||||||
|
head: ["VÖEN", "Налог", "Открыта", "Риск", "Статус"],
|
||||||
|
rows: [
|
||||||
|
["1234567891", "ƏDV", "12.02.2026", "85", "Открыта"],
|
||||||
|
["7778889990", "Mənbədə", "12.03.2026", "91", "Открыта"],
|
||||||
|
["1112223334", "Gəlir", "05.03.2026", "45", "В работе"],
|
||||||
|
],
|
||||||
|
sum: "Открытых: <b>3</b> · Макс. риск: <b>91</b>",
|
||||||
|
},
|
||||||
|
attention: {
|
||||||
|
title: "⚠ Требует внимания — сводка проблем",
|
||||||
|
head: ["Приоритет", "Проблема", "Налог / Область", "Детали", "Срок"],
|
||||||
|
rows: [
|
||||||
|
["СРОЧНО", "Дедлайн завтра!", "Mənfəət vergisi", "Годовая декларация не сдана", "31.03.2026"],
|
||||||
|
["СРОЧНО", "Просрочена декларация", "Mənbədə tutulan", "2 месячных декларации не сданы", "Просрочено!"],
|
||||||
|
["Риск 91", "Открытая проверка", "Mənbədə tutulan", "5 выплат нерезидентам без сертификата резидентства", "VÖEN 7778889990"],
|
||||||
|
["Риск 85", "Открытая проверка", "ƏDV", "3 e-Qaimə без подтверждения от контрагентов", "VÖEN 1234567891"],
|
||||||
|
["Контроль", "e-Qaimə расхождения", "ƏDV", "3 счёт-фактуры не подтверждены контрагентами", "Проверить до 20.04"],
|
||||||
|
["Шкала", "Превышение порога", "Gəlir vergisi", "2 сотрудника выше ₼25K — пересчитать прогрессивную шкалу", "Гулиев, Сулейманов"],
|
||||||
|
["Дедлайн", "Приближается", "DSMF + İTS + İşsizlik", "Ежемесячная уплата — через 16 дней", "15.04.2026"],
|
||||||
|
["Дедлайн", "Приближается", "ƏDV bəyannaməsi", "Ежемесячная декларация — через 21 день", "20.04.2026"],
|
||||||
|
],
|
||||||
|
sum: 'Критических: <b style="color:var(--ins-negative)">4</b> · Предупреждений: <b style="color:var(--ins-warning)">4</b>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
overview: {
|
||||||
|
total: {
|
||||||
|
title: "Все проверки за период",
|
||||||
|
head: ["VÖEN", "Компания", "Налог", "Дата", "Риск", "Статус"],
|
||||||
|
rows: [
|
||||||
|
["1234567891", "AzBuild MMC", "ƏDV", "12.02.2026", "85", "Открыта"],
|
||||||
|
["9876543210", "TradeMax LLC", "Mənfəət", "18.01.2026", "62", "В работе"],
|
||||||
|
["5555666677", "InfoTech MMC", "DSMF", "10.03.2026", "28", "Закрыта"],
|
||||||
|
["1112223334", "FinServ LLC", "Gəlir", "05.03.2026", "45", "Ожидание"],
|
||||||
|
["7778889990", "Import Group", "Mənbədə", "12.03.2026", "91", "Открыта"],
|
||||||
|
["3334445556", "GreenBuild", "Əmlak", "22.02.2026", "22", "Закрыта"],
|
||||||
|
],
|
||||||
|
sum: "Всего: <b>247</b> · Открытых: <b>79</b> · Закрытых: <b>168</b>",
|
||||||
|
},
|
||||||
|
taxsum: {
|
||||||
|
title: "Сумма налогов по видам",
|
||||||
|
head: ["Налог", "Начислено", "Уплачено", "Сальдо", "% от общего"],
|
||||||
|
rows: [
|
||||||
|
["ƏDV", "₼5.8M", "₼5.6M", "₼200K", "31.5%"],
|
||||||
|
["Mənfəət", "₼4.2M", "₼4.2M", "₼0", "22.8%"],
|
||||||
|
["Gəlir", "₼3.1M", "₼3.1M", "₼0", "16.8%"],
|
||||||
|
["DSMF", "₼1.5M", "₼1.5M", "₼0", "8.2%"],
|
||||||
|
["Mənbədə", "₼1.9M", "₼1.7M", "₼200K", "10.3%"],
|
||||||
|
["Прочие", "₼1.9M", "₼1.9M", "₼0", "10.3%"],
|
||||||
|
],
|
||||||
|
sum: "Итого: <b>₼18.4M</b> · Сальдо: <b>₼400K</b>",
|
||||||
|
},
|
||||||
|
saved: {
|
||||||
|
title: "Штрафы предотвращены — детали",
|
||||||
|
head: ["Компания", "Налог", "Потенциальный штраф", "Причина", "Действие"],
|
||||||
|
rows: [
|
||||||
|
["AzBuild MMC", "ƏDV", "₼580K", "Расхождение e-Qaimə", "Сверка проведена"],
|
||||||
|
["TradeMax LLC", "Mənfəət", "₼340K", "Ошибка в декларации", "Корректировка"],
|
||||||
|
["InfoTech MMC", "DSMF", "₼120K", "Неверная база", "Перерасчёт"],
|
||||||
|
["Import Group", "Mənbədə", "₼480K", "Нерезиденты без серт.", "Запрос сертификатов"],
|
||||||
|
["GreenBuild", "Əmlak", "₼90K", "Переоценка ОС", "Актуализация"],
|
||||||
|
],
|
||||||
|
sum: "Предотвращено: <b>₼2.1M</b> штрафов",
|
||||||
|
},
|
||||||
|
inprogress: {
|
||||||
|
title: "Незакрытые проверки",
|
||||||
|
head: ["VÖEN", "Компания", "Налог", "Дней открыта", "Риск"],
|
||||||
|
rows: [
|
||||||
|
["1234567891", "AzBuild MMC", "ƏDV", "47", "85"],
|
||||||
|
["7778889990", "Import Group", "Mənbədə", "18", "91"],
|
||||||
|
["1112223334", "FinServ LLC", "Gəlir", "25", "45"],
|
||||||
|
["2223334445", "LogiTrans", "Aksiz", "62", "38"],
|
||||||
|
["4445556667", "PharmaPlus", "İTS", "34", "32"],
|
||||||
|
],
|
||||||
|
sum: "В процессе: <b>79</b> · Средн. длительность: <b>58 дней</b>",
|
||||||
|
},
|
||||||
|
risk: {
|
||||||
|
title: "Риск-скор — топ компании",
|
||||||
|
head: ["Компания", "VÖEN", "Скор", "Осн. риск", "Уровень"],
|
||||||
|
rows: [
|
||||||
|
["Import Group", "7778889990", "91", "Mənbədə нерезиденты", "Критический"],
|
||||||
|
["AzBuild MMC", "1234567891", "85", "ƏDV e-Qaimə", "Высокий"],
|
||||||
|
["TradeMax LLC", "9876543210", "62", "Mənfəət декларация", "Средний"],
|
||||||
|
["FinServ LLC", "1112223334", "45", "Gəlir шкала", "Средний"],
|
||||||
|
["InfoTech MMC", "5555666677", "28", "DSMF база", "Низкий"],
|
||||||
|
],
|
||||||
|
sum: "Средний скор: <b>78</b> · Критических: <b>2</b>",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tax: {
|
||||||
|
edv: {
|
||||||
|
title: "ƏDV (НДС) — 18%",
|
||||||
|
badge: "18%",
|
||||||
|
head: ["Дата", "Счёт Дт", "Счёт Кт", "Сумма", "Описание"],
|
||||||
|
rows: [
|
||||||
|
["28.02", "211", "521.1", "₼48,200", "ƏDV с продаж"],
|
||||||
|
["28.02", "521.1", "241", "₼31,400", "Зачёт входящего"],
|
||||||
|
["20.03", "521.1", "223", "₼16,800", "Уплата в бюджет"],
|
||||||
|
["31.01", "211", "521.1", "₼52,100", "ƏDV с продаж янв."],
|
||||||
|
["20.02", "521.1", "223", "₼18,300", "Уплата янв."],
|
||||||
|
],
|
||||||
|
sum: "Начислено: <b>₼100,300</b> · Уплачено: <b>₼35,100</b>",
|
||||||
|
},
|
||||||
|
menfeet: {
|
||||||
|
title: "Mənfəət vergisi — 20%",
|
||||||
|
badge: "20%",
|
||||||
|
head: ["Дата", "Счёт Дт", "Счёт Кт", "Сумма", "Описание"],
|
||||||
|
rows: [
|
||||||
|
["31.03", "801", "521.2", "₼84,000", "Начисление за 2025"],
|
||||||
|
["31.03", "521.2", "223", "₼63,000", "Уплата с учётом авансов"],
|
||||||
|
["15.01", "521.2", "223", "₼21,000", "Аванс Q4"],
|
||||||
|
],
|
||||||
|
sum: "Начислено: <b>₼84,000</b> · Авансы: <b>₼63,000</b>",
|
||||||
|
},
|
||||||
|
gelir: {
|
||||||
|
title: "Gəlir vergisi — 0–14%",
|
||||||
|
badge: "0–14%",
|
||||||
|
head: ["ФИО", "Оклад", "Ставка", "Сумма НДФЛ", "Удержано"],
|
||||||
|
rows: [
|
||||||
|
["Мамедов Р.А.", "₼12,000", "14%", "₼560", "✓"],
|
||||||
|
["Гулиев Э.Ф.", "₼26,000", "14%+25%", "₼2,770", "✓"],
|
||||||
|
["Сулейманов Н.И.", "₼28,500", "14%+25%", "₼3,395", "✓"],
|
||||||
|
["Алиева С.А.", "₼9,500", "14%", "₼210", "✓"],
|
||||||
|
["Прочие (20 чел.)", "₼110,400", "0–14%", "₼7,345", "✓"],
|
||||||
|
],
|
||||||
|
sum: 'НДФЛ итого: <b>₼14,280/мес.</b> · <span style="color:var(--ins-negative)">2 чел. выше ₼25K</span>',
|
||||||
|
},
|
||||||
|
dsmf: {
|
||||||
|
title: "DSMF — 22%+3%",
|
||||||
|
badge: "25%",
|
||||||
|
head: ["Дата", "Счёт Дт", "Счёт Кт", "Сумма", "Описание"],
|
||||||
|
rows: [
|
||||||
|
["28.02", "731", "522", "₼41,008", "DSMF раб-ль 22%"],
|
||||||
|
["28.02", "533", "522", "₼5,592", "DSMF раб-к 3%"],
|
||||||
|
["15.03", "522", "223", "₼46,600", "Уплата DSMF"],
|
||||||
|
],
|
||||||
|
sum: "DSMF: <b>₼46,600/мес.</b> · 24 чел.",
|
||||||
|
},
|
||||||
|
its: {
|
||||||
|
title: "İTS — 2%+2%",
|
||||||
|
badge: "4%",
|
||||||
|
head: ["Дата", "Дт", "Кт", "Сумма", "Описание"],
|
||||||
|
rows: [
|
||||||
|
["28.02", "731", "522.1", "₼3,728", "İTS раб-ль 2%"],
|
||||||
|
["28.02", "533", "522.1", "₼3,728", "İTS раб-к 2%"],
|
||||||
|
["15.03", "522.1", "223", "₼7,456", "Уплата İTS"],
|
||||||
|
],
|
||||||
|
sum: "İTS: <b>₼7,456/мес.</b>",
|
||||||
|
},
|
||||||
|
issizlik: {
|
||||||
|
title: "İşsizlik — 0.5%+0.5%",
|
||||||
|
badge: "1%",
|
||||||
|
head: ["Дата", "Дт", "Кт", "Сумма", "Описание"],
|
||||||
|
rows: [
|
||||||
|
["28.02", "731", "522.2", "₼932", "İşsizlik раб-ль"],
|
||||||
|
["28.02", "533", "522.2", "₼932", "İşsizlik раб-к"],
|
||||||
|
["15.03", "522.2", "223", "₼1,864", "Уплата"],
|
||||||
|
],
|
||||||
|
sum: "İşsizlik: <b>₼1,864/мес.</b>",
|
||||||
|
},
|
||||||
|
menbede: {
|
||||||
|
title: "Mənbədə tutulan — 10–15%",
|
||||||
|
badge: "10–15%",
|
||||||
|
head: ["Дата", "Дт", "Кт", "Сумма", "Описание"],
|
||||||
|
rows: [
|
||||||
|
["15.03", "534", "521.5", "₼25,000", "10% дивиденды"],
|
||||||
|
["15.03", "537", "521.5", "₼3,200", "10% проценты"],
|
||||||
|
["10.03", "538", "521.5", "₼7,000", "14% роялти нерезид."],
|
||||||
|
["20.03", "521.5", "223", "₼35,200", "Уплата в бюджет"],
|
||||||
|
],
|
||||||
|
sum: 'Удержано: <b>₼39,700</b> · <span style="color:var(--ins-negative)">5 нерезидентов без серт.!</span>',
|
||||||
|
},
|
||||||
|
sadelesdirilmis: {
|
||||||
|
title: "Sadələşdirilmiş — 2%",
|
||||||
|
badge: "2%",
|
||||||
|
head: ["Квартал", "Оборот", "Налог 2%", "Уплачен", "Статус"],
|
||||||
|
rows: [
|
||||||
|
["Q1", "₼62K", "₼1,240", "₼1,240", "✓"],
|
||||||
|
["Q2", "₼58K", "₼1,160", "₼1,160", "✓"],
|
||||||
|
["Q3", "₼65K", "₼1,300", "₼1,300", "✓"],
|
||||||
|
["Q4", "₼71K", "₼1,420", "—", "Ожидает"],
|
||||||
|
],
|
||||||
|
sum: 'Оборот YTD: <b>₼256K</b> · <span style="color:var(--ins-negative)">Порог ₼200K — ƏDV регистрация!</span>',
|
||||||
|
},
|
||||||
|
aksiz: {
|
||||||
|
title: "Aksiz — ставки",
|
||||||
|
badge: "Фикс.",
|
||||||
|
head: ["Период", "Сумма", "Статус"],
|
||||||
|
rows: [
|
||||||
|
["Q1", "₼150K", "✓"],
|
||||||
|
["Q2", "₼148K", "✓"],
|
||||||
|
["Q3", "₼155K", "✓"],
|
||||||
|
["Q4", "₼147K", "Ожид."],
|
||||||
|
],
|
||||||
|
sum: "Итого: <b>₼600K</b>",
|
||||||
|
},
|
||||||
|
yol: {
|
||||||
|
title: "Yol vergisi",
|
||||||
|
badge: "Фикс.",
|
||||||
|
head: ["Авто", "Объём", "Ставка", "Сумма"],
|
||||||
|
rows: [
|
||||||
|
["Toyota Camry", "2.5L", "₼200", "₼200"],
|
||||||
|
["Mercedes Sprinter", "2.2L", "₼180", "₼180"],
|
||||||
|
],
|
||||||
|
sum: "Итого: <b>₼380</b>/год",
|
||||||
|
},
|
||||||
|
meden: {
|
||||||
|
title: "Mədən vergisi",
|
||||||
|
badge: "%",
|
||||||
|
head: ["Период", "Сумма", "Статус"],
|
||||||
|
rows: [["2025", "₼100K", "✓"]],
|
||||||
|
sum: "Итого: <b>₼100K</b>",
|
||||||
|
},
|
||||||
|
emlak: {
|
||||||
|
title: "Əmlak vergisi — 1%",
|
||||||
|
badge: "1%",
|
||||||
|
head: ["Квартал", "База ОС", "Налог 1%", "Статус"],
|
||||||
|
rows: [
|
||||||
|
["Q1", "₼10M", "₼25K", "✓"],
|
||||||
|
["Q2", "₼9.8M", "₼24.5K", "✓"],
|
||||||
|
["Q3", "₼9.6M", "₼24K", "✓"],
|
||||||
|
["Q4", "₼9.4M", "₼23.5K", "✓"],
|
||||||
|
],
|
||||||
|
sum: "Итого: <b>₼97K</b> · Совет: переоценка ОС",
|
||||||
|
},
|
||||||
|
torpaq: {
|
||||||
|
title: "Torpaq vergisi",
|
||||||
|
badge: "Фикс.",
|
||||||
|
head: ["Участок", "Площадь", "Зона", "Налог"],
|
||||||
|
rows: [
|
||||||
|
["Офис Баку", "450 м²", "Центр", "₼180K"],
|
||||||
|
["Склад Сумг.", "1200 м²", "Промзона", "₼120K"],
|
||||||
|
],
|
||||||
|
sum: "Итого: <b>₼300K</b>/год",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
industry: {
|
||||||
|
assets: {
|
||||||
|
title: "Активы — группы счетов (01–29)",
|
||||||
|
head: ["Счёт", "Название", "Оборот Дт", "Оборот Кт", "Сальдо"],
|
||||||
|
rows: [
|
||||||
|
["011", "Torpaq, tikili və avadanlıqlar", "₼1,240K", "₼180K", "₼9,860K"],
|
||||||
|
["021", "Qeyri-maddi aktivlər", "₼85K", "₼12K", "₼320K"],
|
||||||
|
["101", "İstehsalat ehtiyatları", "₼480K", "₼420K", "₼185K"],
|
||||||
|
["211", "Alıcılar və sifarişçilər", "₼3,200K", "₼2,860K", "₼340K"],
|
||||||
|
["221", "Qısamüddətli debitor borclar", "₼560K", "₼480K", "₼120K"],
|
||||||
|
["223", "Bank hesabları", "₼8,400K", "₼7,250K", "₼1,580K"],
|
||||||
|
["226", "ƏDV depozit hesabı", "₼45K", "₼30K", "₼15K"],
|
||||||
|
["241", "ƏDV əvəzləşdirilməsi", "₼680K", "₼680K", "₼0"],
|
||||||
|
],
|
||||||
|
sum: "Итого активы: <b>₼12.4M</b> · 8 групп · 142 проводки",
|
||||||
|
},
|
||||||
|
liabilities: {
|
||||||
|
title: "Обязательства — группы счетов (40–55)",
|
||||||
|
head: ["Счёт", "Название", "Оборот Дт", "Оборот Кт", "Сальдо"],
|
||||||
|
rows: [
|
||||||
|
["431", "Uzunmüddətli bank kreditləri", "₼120K", "₼0", "₼2,400K"],
|
||||||
|
["501", "Qısamüddətli bank kreditləri", "₼350K", "₼500K", "₼850K"],
|
||||||
|
["521", "Vergi öhdəlikləri", "₼1,840K", "₼1,920K", "₼1,420K"],
|
||||||
|
["522", "DSMF / İTS öhdəlikləri", "₼560K", "₼560K", "₼46.6K"],
|
||||||
|
["531", "Malsatan və podratçılar", "₼2,100K", "₼2,350K", "₼1,680K"],
|
||||||
|
["533", "Əmək haqqı üzrə öhdəliklər", "₼2,232K", "₼2,232K", "₼186K"],
|
||||||
|
["545", "Digər qısamüddətli öhdəliklər", "₼180K", "₼240K", "₼620K"],
|
||||||
|
],
|
||||||
|
sum: "Итого обязательства: <b>₼7.2M</b> · 7 групп · 98 проводок",
|
||||||
|
},
|
||||||
|
equity: {
|
||||||
|
title: "Капитал — группы счетов (30–34)",
|
||||||
|
head: ["Счёт", "Название", "Оборот Дт", "Оборот Кт", "Сальдо"],
|
||||||
|
rows: [
|
||||||
|
["301", "Nizamnamə kapitalı", "₼0", "₼0", "₼3,000K"],
|
||||||
|
["331", "Hesabat dövrünün mənfəəti", "₼0", "₼840K", "₼840K"],
|
||||||
|
["332", "Keçmiş illərin mənfəəti", "₼250K", "₼0", "₼1,200K"],
|
||||||
|
["341", "Hesabat dövrü üzrə xərclər", "₼160K", "₼0", "₼160K"],
|
||||||
|
],
|
||||||
|
sum: "Итого капитал: <b>₼5.2M</b> · 4 группы · 18 проводок",
|
||||||
|
},
|
||||||
|
income: {
|
||||||
|
title: "Доходы — группы счетов (60–69)",
|
||||||
|
head: ["Счёт", "Название", "Оборот Дт", "Оборот Кт", "Сальдо"],
|
||||||
|
rows: [
|
||||||
|
["601", "Satış gəlirləri", "₼0", "₼4,200K", "₼4,200K"],
|
||||||
|
["611", "Sair əməliyyat gəlirləri", "₼0", "₼380K", "₼380K"],
|
||||||
|
["621", "Maliyyə gəlirləri", "₼0", "₼185K", "₼185K"],
|
||||||
|
["631", "Fövqəladə gəlirlər", "₼0", "₼35K", "₼35K"],
|
||||||
|
],
|
||||||
|
sum: "Итого доходы: <b>₼4.8M</b> · 4 группы · 86 проводок",
|
||||||
|
},
|
||||||
|
expenses: {
|
||||||
|
title: "Расходы — группы счетов (70–80)",
|
||||||
|
head: ["Счёт", "Название", "Оборот Дт", "Оборот Кт", "Сальдо"],
|
||||||
|
rows: [
|
||||||
|
["701", "Satışın maya dəyəri", "₼1,680K", "₼0", "₼1,680K"],
|
||||||
|
["711", "Kommersiya xərcləri", "₼420K", "₼0", "₼420K"],
|
||||||
|
["721", "İnzibati xərclər", "₼890K", "₼0", "₼890K"],
|
||||||
|
["731", "Əmək haqqı xərcləri", "₼560K", "₼0", "₼560K"],
|
||||||
|
["801", "Mənfəət vergisi xərcləri", "₼84K", "₼0", "₼84K"],
|
||||||
|
["741", "Sair əməliyyat xərcləri", "₼266K", "₼0", "₼266K"],
|
||||||
|
],
|
||||||
|
sum: "Итого расходы: <b>₼3.9M</b> · 6 групп · 124 проводки",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
region: {
|
||||||
|
rates: {
|
||||||
|
title: "Налоговые ставки — ВКРП 2026",
|
||||||
|
head: ["Налог", "Статья НК", "Ставка", "Примечание", "Действует с"],
|
||||||
|
rows: [
|
||||||
|
["ƏDV (НДС)", "Ст. 175", "18%", "Стандартная ставка", "01.01.2026"],
|
||||||
|
["Mənfəət vergisi", "Ст. 105.1", "20%", "Юр. лица", "01.01.2026"],
|
||||||
|
["Gəlir (до ₼8K)", "Ст. 101.1", "0%", "Необлагаемый минимум", "01.01.2026"],
|
||||||
|
["Gəlir (₼8K–₼30K)", "Ст. 101.1", "14%", "Основная ставка", "01.01.2026"],
|
||||||
|
["Gəlir (свыше ₼30K)", "Ст. 101.1", "25%", "Повышенная", "01.01.2026"],
|
||||||
|
["Sadələşdirilmiş", "Ст. 218", "2%", "Упрощённый режим", "01.01.2026"],
|
||||||
|
["Əmlak vergisi (юр.)", "Ст. 199", "1%", "От остаточной стоимости", "01.01.2026"],
|
||||||
|
["Torpaq vergisi", "Ст. 206", "₼0.06/м²", "Зависит от зоны", "01.01.2026"],
|
||||||
|
["Mənbədə tutulan", "Ст. 150", "10%", "Дивиденды, проценты", "01.01.2026"],
|
||||||
|
],
|
||||||
|
sum: "Источник: <b>Налоговый кодекс АР</b> · Редакция 2026",
|
||||||
|
},
|
||||||
|
social: {
|
||||||
|
title: "Социальные взносы — 2026",
|
||||||
|
head: ["Взнос", "Работодатель", "Работник", "Итого", "База"],
|
||||||
|
rows: [
|
||||||
|
["DSMF (пенсия)", "22%", "3%", "25%", "Начисленная ЗП"],
|
||||||
|
["İTS (страхование)", "2%", "2%", "4%", "Начисленная ЗП"],
|
||||||
|
["İşsizlik (безработица)", "0.5%", "0.5%", "1%", "Начисленная ЗП"],
|
||||||
|
["<b>ИТОГО</b>", "<b>24.5%</b>", "<b>5.5%</b>", "<b>30%</b>", "—"],
|
||||||
|
],
|
||||||
|
sum: "Закон: <b>О социальном страховании</b>",
|
||||||
|
},
|
||||||
|
limits: {
|
||||||
|
title: "Пороги и лимиты — 2026",
|
||||||
|
head: ["Параметр", "Значение", "Основание", "Действует с"],
|
||||||
|
rows: [
|
||||||
|
["Минимальная зарплата (МЗП)", "₼400", "Указ Президента", "01.01.2026"],
|
||||||
|
["Порог ƏDV регистрации", "₼200,000", "Ст. 155 НК", "01.01.2026"],
|
||||||
|
["Необлагаемый Gəlir (год)", "₼8,000", "Ст. 102.1 НК", "01.01.2026"],
|
||||||
|
["Лимит Sadələşdirilmiş", "₼200,000", "Ст. 218 НК", "01.01.2026"],
|
||||||
|
["Лимит наличных расчётов", "₼30,000", "Ст. 16.1 НК", "01.01.2026"],
|
||||||
|
["Амортизация — здания", "7%", "Ст. 114 НК", "01.01.2026"],
|
||||||
|
["Амортизация — оборудование", "20%", "Ст. 114 НК", "01.01.2026"],
|
||||||
|
["Амортизация — транспорт", "25%", "Ст. 114 НК", "01.01.2026"],
|
||||||
|
],
|
||||||
|
sum: "Клик на значение для редактирования",
|
||||||
|
},
|
||||||
|
inspection: {
|
||||||
|
title: "Параметры инспекции",
|
||||||
|
head: ["Параметр", "Значение", "Описание"],
|
||||||
|
rows: [
|
||||||
|
["Риск-скор: высокий", ">80", "Красная зона — срочная проверка"],
|
||||||
|
["Риск-скор: средний", "50–80", "Жёлтая зона — наблюдение"],
|
||||||
|
["Риск-скор: низкий", "<50", "Зелёная зона — норма"],
|
||||||
|
["Дедлайн ƏDV", "20 числа сл. мес.", "Ежемесячная подача"],
|
||||||
|
["Дедлайн Mənfəət", "31 марта", "Годовая декларация"],
|
||||||
|
["Дедлайн DSMF", "15 числа сл. мес.", "Ежемесячный платёж"],
|
||||||
|
["Частота проверок ƏDV", "Ежемесячно", "Автоматическая сверка"],
|
||||||
|
["Частота проверок Mənfəət", "Ежеквартально", "По закрытии периода"],
|
||||||
|
["Уведомление за дней", "5", "До дедлайна"],
|
||||||
|
],
|
||||||
|
sum: "Клик на значение для редактирования",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Pulse page chart data
|
||||||
|
charts: {
|
||||||
|
months: ["Янв", "Фев", "Мар", "Апр", "Май", "Июн", "Июл", "Авг", "Сен", "Окт", "Ноя", "Дек"],
|
||||||
|
revenue: [320, 355, 410, 380, 365, 340, 360, 350, 390, 370, 345, 315],
|
||||||
|
taxes: [48, 52, 61, 55, 54, 49, 53, 51, 58, 54, 50, 45],
|
||||||
|
taxesPaid: [40, 45, 55, 50, 48, 44, 48, 46, 52, 49, 45, 40],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Pulse KPI values
|
||||||
|
kpi: {
|
||||||
|
revenue: { value: "₼4.2M", change: "+12%", up: true },
|
||||||
|
taxload: { value: "18.4%", change: "+1.2%", up: false },
|
||||||
|
employees: { value: "24", change: "+3", up: true },
|
||||||
|
declarations: { value: "8/13", change: "5 ожид.", up: false },
|
||||||
|
inspections: { value: "3", change: "Риск 91", up: false },
|
||||||
|
},
|
||||||
|
|
||||||
|
// Deadlines for Pulse page
|
||||||
|
deadlines: [
|
||||||
|
{ icon: "📋", title: "Mənfəət vergisi — годовая", date: "31.03.2026", days: 1, urgency: "urgent" },
|
||||||
|
{ icon: "💰", title: "DSMF + İTS + İşsizlik", date: "15.04.2026", days: 16, urgency: "urgent" },
|
||||||
|
{ icon: "📋", title: "ƏDV bəyannaməsi", date: "20.04.2026", days: 21, urgency: "soon" },
|
||||||
|
{ icon: "💰", title: "Əmlak — аванс Q2", date: "15.05.2026", days: 46, urgency: "ok" },
|
||||||
|
],
|
||||||
|
|
||||||
|
// Declaration status for segmented bars
|
||||||
|
declarationStatus: [
|
||||||
|
{ name: "ƏDV", filed: 12, total: 12, late: 0 },
|
||||||
|
{ name: "Gəlir vergisi", filed: 12, total: 12, late: 0 },
|
||||||
|
{ name: "DSMF / İTS", filed: 11, total: 12, late: 0 },
|
||||||
|
{ name: "Mənfəət", filed: 3, total: 4, late: 0 },
|
||||||
|
{ name: "Mənbədə", filed: 10, total: 12, late: 2 },
|
||||||
|
],
|
||||||
|
|
||||||
|
// Tax structure for horizontal bars
|
||||||
|
taxStructure: [
|
||||||
|
{ name: "ƏDV", value: 5.8, pct: 31.5, color: "#4f46e5" },
|
||||||
|
{ name: "Mənfəət", value: 4.2, pct: 22.8, color: "#7c3aed" },
|
||||||
|
{ name: "Gəlir", value: 3.1, pct: 16.8, color: "#2563eb" },
|
||||||
|
{ name: "Mənbədə", value: 1.9, pct: 10.3, color: "#dc2626" },
|
||||||
|
{ name: "DSMF", value: 1.5, pct: 8.2, color: "#059669" },
|
||||||
|
{ name: "Прочие", value: 1.9, pct: 10.3, color: "#94a3b8" },
|
||||||
|
],
|
||||||
|
|
||||||
|
// Health score
|
||||||
|
healthScore: 75,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { reactive, computed } from "vue"
|
||||||
|
import { D } from "./mockData"
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
data: D,
|
||||||
|
activeFilters: {},
|
||||||
|
period: {
|
||||||
|
year: "2025",
|
||||||
|
quarters: ["Q4"],
|
||||||
|
months: [],
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
export function useStore() {
|
||||||
|
const getData = (ctx, key) => {
|
||||||
|
return state.data[ctx]?.[key] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
const getKpi = (key) => {
|
||||||
|
return state.data.kpi?.[key] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
const setLoading = (val) => {
|
||||||
|
state.loading = val
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSetting = (key, value) => {
|
||||||
|
// For now, just local mutation
|
||||||
|
// Later: frappe.call to persist
|
||||||
|
console.log(`Setting ${key} = ${value}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
getData,
|
||||||
|
getKpi,
|
||||||
|
setLoading,
|
||||||
|
updateSetting,
|
||||||
|
D: state.data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
@import "frappe-ui/src/style.css";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg-base: #f1f5f9;
|
||||||
|
--bg-page: #f8fafc;
|
||||||
|
--bg-card: #fff;
|
||||||
|
--border: #e2e8f0;
|
||||||
|
--border-light: #f1f5f9;
|
||||||
|
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||||
|
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.08);
|
||||||
|
--shadow-press: inset 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||||
|
--ins-indigo: #4f46e5;
|
||||||
|
--ins-indigo-light: #e0e7ff;
|
||||||
|
--ins-indigo-dim: rgba(79, 70, 229, 0.08);
|
||||||
|
--ins-indigo-border: rgba(79, 70, 229, 0.25);
|
||||||
|
--ins-positive: #059669;
|
||||||
|
--ins-positive-bg: #ecfdf5;
|
||||||
|
--ins-negative: #dc2626;
|
||||||
|
--ins-negative-bg: #fef2f2;
|
||||||
|
--ins-warning: #d97706;
|
||||||
|
--ins-warning-bg: #fffbeb;
|
||||||
|
--ins-info: #2563eb;
|
||||||
|
--ins-info-bg: #eff6ff;
|
||||||
|
--ins-text-primary: #0f172a;
|
||||||
|
--ins-text-secondary: #475569;
|
||||||
|
--ins-text-tertiary: #94a3b8;
|
||||||
|
--ins-radius: 12px;
|
||||||
|
--ins-radius-sm: 8px;
|
||||||
|
--ins-radius-xs: 6px;
|
||||||
|
--ins-header-h: 56px;
|
||||||
|
--ins-sidebar-w: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "Manrope", -apple-system, sans-serif;
|
||||||
|
background: var(--bg-page);
|
||||||
|
color: var(--ins-text-primary);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glassmorphism utility */
|
||||||
|
.glass {
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
|
background: rgba(255, 255, 255, 0.65);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||||
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.04), inset 0 1px 0 rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes pulseDanger {
|
||||||
|
0%, 100% { box-shadow: var(--shadow-sm), 0 0 0 0 rgba(220, 38, 38, 0.15); }
|
||||||
|
50% { box-shadow: var(--shadow-sm), 0 0 0 6px rgba(220, 38, 38, 0.06); }
|
||||||
|
}
|
||||||
|
@keyframes pulseWarning {
|
||||||
|
0%, 100% { box-shadow: var(--shadow-sm), 0 0 0 0 rgba(217, 119, 6, 0.15); }
|
||||||
|
50% { box-shadow: var(--shadow-sm), 0 0 0 6px rgba(217, 119, 6, 0.06); }
|
||||||
|
}
|
||||||
|
@keyframes donutPulse {
|
||||||
|
0%, 100% { filter: drop-shadow(0 0 8px rgba(79, 70, 229, 0.15)); }
|
||||||
|
50% { filter: drop-shadow(0 0 20px rgba(79, 70, 229, 0.35)); }
|
||||||
|
}
|
||||||
|
@keyframes donutBreathe {
|
||||||
|
0%, 100% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.02); }
|
||||||
|
}
|
||||||
|
@keyframes ringSpin {
|
||||||
|
from { transform: rotate(-90deg); }
|
||||||
|
to { transform: rotate(270deg); }
|
||||||
|
}
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { background-position: -200% 0; }
|
||||||
|
100% { background-position: 200% 0; }
|
||||||
|
}
|
||||||
|
@keyframes dotPulse {
|
||||||
|
0%, 100% { transform: scale(1); opacity: 1; }
|
||||||
|
50% { transform: scale(1.5); opacity: 0.5; }
|
||||||
|
}
|
||||||
|
@keyframes fadeSlideIn {
|
||||||
|
from { opacity: 0; transform: translateY(12px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
@keyframes timelinePulse {
|
||||||
|
0%, 100% { box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.3); }
|
||||||
|
50% { box-shadow: 0 0 0 5px rgba(220, 38, 38, 0.08); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-pulse-danger {
|
||||||
|
animation: pulseDanger 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.animate-pulse-warning {
|
||||||
|
animation: pulseWarning 3.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.animate-shimmer {
|
||||||
|
background: linear-gradient(90deg, var(--bg-base) 25%, #e2e8f0 50%, var(--bg-base) 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.2s infinite;
|
||||||
|
}
|
||||||
|
.animate-fade-slide-in {
|
||||||
|
animation: fadeSlideIn 0.45s cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content swap states */
|
||||||
|
.content-swap {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
.content-swap.fading-out {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.97) translateY(-6px);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.35s ease, transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
}
|
||||||
|
.content-swap.gone {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.content-swap.fading-in {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(16px);
|
||||||
|
}
|
||||||
|
.content-swap.revealing {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
transition: opacity 0.6s cubic-bezier(0.22, 1, 0.36, 1), transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Staggered row reveal */
|
||||||
|
.row-stagger {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
animation: fadeSlideIn 0.4s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inspect button */
|
||||||
|
.inspect-btn {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
background: #059669;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 3px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: 0 4px 16px rgba(5, 150, 105, 0.3);
|
||||||
|
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.3s ease, background 0.2s ease;
|
||||||
|
}
|
||||||
|
.inspect-btn:hover {
|
||||||
|
transform: scale(1.08);
|
||||||
|
background: #047857;
|
||||||
|
box-shadow: 0 6px 24px rgba(5, 150, 105, 0.4);
|
||||||
|
}
|
||||||
|
.inspect-btn:active {
|
||||||
|
transform: scale(0.92);
|
||||||
|
transition: transform 60ms ease;
|
||||||
|
}
|
||||||
|
.inspect-btn.loading {
|
||||||
|
background: #047857;
|
||||||
|
}
|
||||||
|
.inspect-btn svg {
|
||||||
|
display: block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.inspect-btn span {
|
||||||
|
font-family: 'Manrope', sans-serif;
|
||||||
|
font-size: 8px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
line-height: 1;
|
||||||
|
color: #fff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar */
|
||||||
|
::-webkit-scrollbar { width: 6px; }
|
||||||
|
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
|
||||||
|
|
||||||
|
/* Mono text for data values */
|
||||||
|
.font-data {
|
||||||
|
font-family: "Source Code Pro", monospace;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { createApp } from "vue"
|
||||||
|
import App from "./App.vue"
|
||||||
|
import router from "./router"
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
setConfig,
|
||||||
|
frappeRequest,
|
||||||
|
resourcesPlugin,
|
||||||
|
} from "frappe-ui"
|
||||||
|
|
||||||
|
import "./main.css"
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
setConfig("resourceFetcher", frappeRequest)
|
||||||
|
app.use(resourcesPlugin)
|
||||||
|
|
||||||
|
app.component("Button", Button)
|
||||||
|
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
router.isReady().then(async () => {
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
await frappeRequest({
|
||||||
|
url: "/api/method/inspector.www.inspector.get_context_for_dev",
|
||||||
|
}).then((values) => {
|
||||||
|
if (!window.frappe) window.frappe = {}
|
||||||
|
window.frappe.boot = values
|
||||||
|
})
|
||||||
|
}
|
||||||
|
app.mount("#app")
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
<template>
|
||||||
|
<div class="animate-fade-slide-in">
|
||||||
|
<FilterBar
|
||||||
|
title="ПЛАН СЧЕТОВ"
|
||||||
|
:groups="filterGroups"
|
||||||
|
v-model="filters"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="content-swap mb-5" :class="cardsClass">
|
||||||
|
<div class="grid grid-cols-5 gap-3">
|
||||||
|
<KpiCard
|
||||||
|
v-for="card in visibleCards"
|
||||||
|
:key="card.key"
|
||||||
|
v-bind="card"
|
||||||
|
@click="onCardClick(card.key)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DetailPanel
|
||||||
|
:open="detailOpen"
|
||||||
|
:title="detailData?.title || ''"
|
||||||
|
:columns="detailData?.head || []"
|
||||||
|
:rows="detailData?.rows || []"
|
||||||
|
:summary="detailData?.sum || ''"
|
||||||
|
@close="closeDetail"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from "vue"
|
||||||
|
import FilterBar from "@/components/shared/FilterBar.vue"
|
||||||
|
import KpiCard from "@/components/shared/KpiCard.vue"
|
||||||
|
import DetailPanel from "@/components/shared/DetailPanel.vue"
|
||||||
|
import { useStore } from "@/data/store"
|
||||||
|
import { useContentSwap } from "@/utils/useContentSwap"
|
||||||
|
|
||||||
|
const { D } = useStore()
|
||||||
|
const { cardsClass, detailOpen, openDetail, closeDetail } = useContentSwap()
|
||||||
|
|
||||||
|
const filters = ref({})
|
||||||
|
const detailData = ref(null)
|
||||||
|
|
||||||
|
const filterGroups = [
|
||||||
|
{
|
||||||
|
chips: [
|
||||||
|
{ key: "assets", label: "Активы" },
|
||||||
|
{ key: "liabilities", label: "Обязательства" },
|
||||||
|
{ key: "equity", label: "Капитал" },
|
||||||
|
{ key: "income", label: "Доходы" },
|
||||||
|
{ key: "expenses", label: "Расходы" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const allCards = [
|
||||||
|
{ key: "assets", label: "Активы", value: "₼12.4M", subtitle: "Счета 01–29", change: "+8%", changeUp: true, status: "normal" },
|
||||||
|
{ key: "liabilities", label: "Обязательства", value: "₼7.2M", subtitle: "Счета 40–55", change: "+3%", changeUp: false, status: "normal" },
|
||||||
|
{ key: "equity", label: "Капитал", value: "₼5.2M", subtitle: "Счета 30–34", change: "+12%", changeUp: true, status: "normal" },
|
||||||
|
{ key: "income", label: "Доходы", value: "₼4.8M", subtitle: "Счета 60–69", change: "+15%", changeUp: true, status: "normal" },
|
||||||
|
{ key: "expenses", label: "Расходы", value: "₼3.9M", subtitle: "Счета 70–80", change: "+6%", changeUp: false, status: "normal" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const visibleCards = computed(() =>
|
||||||
|
allCards.filter((c) => filters.value[c.key] !== false)
|
||||||
|
)
|
||||||
|
|
||||||
|
function onCardClick(key) {
|
||||||
|
detailData.value = D.industry[key] || null
|
||||||
|
if (detailData.value) openDetail()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
<template>
|
||||||
|
<div class="animate-fade-slide-in">
|
||||||
|
<FilterBar
|
||||||
|
title="ПОКАЗАТЕЛИ"
|
||||||
|
:groups="filterGroups"
|
||||||
|
v-model="filters"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="content-swap mb-5" :class="cardsClass">
|
||||||
|
<div class="grid grid-cols-5 gap-3">
|
||||||
|
<KpiCard
|
||||||
|
v-for="card in visibleCards"
|
||||||
|
:key="card.key"
|
||||||
|
v-bind="card"
|
||||||
|
@click="onCardClick(card.key)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DetailPanel
|
||||||
|
:open="detailOpen"
|
||||||
|
:title="detailData?.title || ''"
|
||||||
|
:columns="detailData?.head || []"
|
||||||
|
:rows="detailData?.rows || []"
|
||||||
|
:summary="detailData?.sum || ''"
|
||||||
|
@close="closeDetail"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from "vue"
|
||||||
|
import FilterBar from "@/components/shared/FilterBar.vue"
|
||||||
|
import KpiCard from "@/components/shared/KpiCard.vue"
|
||||||
|
import DetailPanel from "@/components/shared/DetailPanel.vue"
|
||||||
|
import { useStore } from "@/data/store"
|
||||||
|
import { useContentSwap } from "@/utils/useContentSwap"
|
||||||
|
|
||||||
|
const { D } = useStore()
|
||||||
|
const { cardsClass, detailOpen, openDetail, closeDetail } = useContentSwap()
|
||||||
|
|
||||||
|
const filters = ref({})
|
||||||
|
const detailData = ref(null)
|
||||||
|
|
||||||
|
const filterGroups = [
|
||||||
|
{
|
||||||
|
chips: [
|
||||||
|
{ key: "total", label: "Проверки" },
|
||||||
|
{ key: "taxsum", label: "Налоги" },
|
||||||
|
{ key: "saved", label: "Штрафы" },
|
||||||
|
{ key: "inprogress", label: "В процессе" },
|
||||||
|
{ key: "risk", label: "Риск" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const allCards = [
|
||||||
|
{ key: "total", label: "Всего проверок", value: "247", subtitle: "за период", change: "+12%", changeUp: true, status: "normal" },
|
||||||
|
{ key: "taxsum", label: "Сумма налогов", value: "₼18.4M", subtitle: "начислено", change: "+8.3%", changeUp: true, status: "normal" },
|
||||||
|
{ key: "saved", label: "Штрафы предотвр.", value: "₼2.1M", subtitle: "экономия", change: "+23%", changeUp: true, status: "normal" },
|
||||||
|
{ key: "inprogress", label: "В процессе", value: "79", subtitle: "незакрытых", change: "32.2%", changeUp: false, status: "warning" },
|
||||||
|
{ key: "risk", label: "Риск-скор", value: "78", subtitle: "внимание", change: "", changeUp: false, status: "critical" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const visibleCards = computed(() =>
|
||||||
|
allCards.filter((c) => filters.value[c.key] !== false)
|
||||||
|
)
|
||||||
|
|
||||||
|
function onCardClick(key) {
|
||||||
|
detailData.value = D.overview[key] || null
|
||||||
|
if (detailData.value) openDetail()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,327 @@
|
||||||
|
<template>
|
||||||
|
<div class="animate-fade-slide-in relative">
|
||||||
|
<!-- Header row -->
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-xl font-extrabold text-[var(--ins-text-primary)]">
|
||||||
|
Пульс компании
|
||||||
|
</h1>
|
||||||
|
<p class="text-sm text-[var(--ins-text-tertiary)] font-medium mt-0.5">
|
||||||
|
Jey Soft MMC · Обзор налогового здоровья
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3.5">
|
||||||
|
<!-- Date range (always shown after first inspect) -->
|
||||||
|
<span
|
||||||
|
v-if="inspected"
|
||||||
|
class="font-manrope text-sm font-bold text-[var(--ins-text-primary)] tracking-tight"
|
||||||
|
>
|
||||||
|
{{ dateRange }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Period button (always available) -->
|
||||||
|
<div class="relative" ref="periodWrap">
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-1.5 px-3.5 py-[7px] rounded-lg glass cursor-pointer font-manrope text-xs font-bold text-[var(--ins-indigo)] transition-all duration-150 hover:border-[var(--ins-indigo-border)] hover:shadow-md"
|
||||||
|
@click.stop="popoverOpen = !popoverOpen"
|
||||||
|
>
|
||||||
|
📅 Период
|
||||||
|
<svg width="10" height="6" viewBox="0 0 10 6" fill="none">
|
||||||
|
<path d="M1 1L5 5L9 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<!-- Popover -->
|
||||||
|
<div
|
||||||
|
v-if="popoverOpen"
|
||||||
|
class="absolute top-[calc(100%+8px)] right-0 w-[340px] bg-white/95 backdrop-blur-[20px] border border-[var(--border)] rounded-xl p-4 z-[999] animate-fade-slide-in"
|
||||||
|
style="box-shadow: 0 16px 48px rgba(0,0,0,.15), 0 4px 12px rgba(0,0,0,.08)"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<h4 class="text-[10px] font-bold text-[var(--ins-text-tertiary)] uppercase tracking-wider mb-2">Год</h4>
|
||||||
|
<div class="flex flex-wrap gap-[5px] mb-3.5">
|
||||||
|
<button
|
||||||
|
v-for="y in ['2024','2025','2026']" :key="y"
|
||||||
|
class="px-2.5 py-1 rounded-full text-[11px] font-semibold border transition-all duration-150"
|
||||||
|
:class="period.year === y ? 'bg-[var(--ins-indigo-light)] border-[var(--ins-indigo-border)] text-[var(--ins-indigo)]' : 'bg-white border-[var(--border)] text-[var(--ins-text-tertiary)]'"
|
||||||
|
@click="period.year = y"
|
||||||
|
>{{ y }}</button>
|
||||||
|
</div>
|
||||||
|
<h4 class="text-[10px] font-bold text-[var(--ins-text-tertiary)] uppercase tracking-wider mb-2">Квартал</h4>
|
||||||
|
<div class="flex flex-wrap gap-[5px] mb-3.5">
|
||||||
|
<button
|
||||||
|
v-for="q in ['Q1','Q2','Q3','Q4']" :key="q"
|
||||||
|
class="px-2.5 py-1 rounded-full text-[11px] font-semibold border transition-all duration-150"
|
||||||
|
:class="period.quarters.includes(q) ? 'bg-[var(--ins-indigo-light)] border-[var(--ins-indigo-border)] text-[var(--ins-indigo)]' : 'bg-white border-[var(--border)] text-[var(--ins-text-tertiary)]'"
|
||||||
|
@click="toggleQ(q)"
|
||||||
|
>{{ q }}</button>
|
||||||
|
</div>
|
||||||
|
<h4 class="text-[10px] font-bold text-[var(--ins-text-tertiary)] uppercase tracking-wider mb-2">Месяц</h4>
|
||||||
|
<div class="flex flex-wrap gap-[5px] mb-4">
|
||||||
|
<button
|
||||||
|
v-for="(m, mi) in monthLabels" :key="m"
|
||||||
|
class="px-2.5 py-1 rounded-full text-[11px] font-semibold border transition-all duration-150"
|
||||||
|
:class="isMonthActive(mi) ? 'bg-[var(--ins-indigo-light)] border-[var(--ins-indigo-border)] text-[var(--ins-indigo)]' : 'bg-white border-[var(--border)] text-[var(--ins-text-tertiary)]'"
|
||||||
|
@click="toggleMonth(mi)"
|
||||||
|
>{{ m }}</button>
|
||||||
|
</div>
|
||||||
|
<!-- OK button -->
|
||||||
|
<button
|
||||||
|
class="w-full py-2 rounded-lg text-xs font-bold text-white transition-all duration-150 hover:opacity-90 active:scale-[0.98]"
|
||||||
|
style="background: linear-gradient(135deg, var(--ins-indigo), #7c3aed)"
|
||||||
|
@click="popoverOpen = false"
|
||||||
|
>
|
||||||
|
OK
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Inspect button -->
|
||||||
|
<button
|
||||||
|
class="inspect-btn"
|
||||||
|
:class="loading ? 'loading' : ''"
|
||||||
|
@click="doInspect"
|
||||||
|
>
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M22 12h-4l-3 9L9 3l-3 9H2" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
<span>Inspect</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div
|
||||||
|
class="content-swap"
|
||||||
|
:class="cardsClass"
|
||||||
|
:style="{ opacity: loading ? 0.3 : undefined, transition: loading ? 'opacity 0.4s ease' : undefined }"
|
||||||
|
>
|
||||||
|
<!-- Row 1: KPI Circles -->
|
||||||
|
<div class="flex justify-between gap-3 mb-6">
|
||||||
|
<PulseKpiCircle
|
||||||
|
v-for="kpi in kpis" :key="kpi.key"
|
||||||
|
:label="kpi.label" :value="kpi.value" :change="kpi.change"
|
||||||
|
:up="kpi.up" :color="kpi.color" :skeleton="loading"
|
||||||
|
:value-style="kpi.valueStyle || ''"
|
||||||
|
@click="onOpenDetail('pulse', kpi.key)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 2: Trend chart + Health Score -->
|
||||||
|
<div class="grid grid-cols-[2fr_1fr] gap-3 mb-3.5">
|
||||||
|
<div class="glass rounded-xl p-5">
|
||||||
|
<div class="text-[13px] font-bold text-[var(--ins-text-primary)] mb-4 flex items-center justify-between">
|
||||||
|
Тренд: Выручка vs Налоги
|
||||||
|
<span class="text-[10px] font-semibold text-[var(--ins-text-tertiary)] px-2.5 py-[3px] bg-[var(--bg-base)] rounded-full">12 мес.</span>
|
||||||
|
</div>
|
||||||
|
<svg class="w-full overflow-visible" viewBox="0 0 500 160" preserveAspectRatio="none">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="revGrad" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="var(--ins-indigo)" stop-opacity="0.12"/>
|
||||||
|
<stop offset="100%" stop-color="var(--ins-indigo)" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="taxGrad2" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="var(--ins-warning)" stop-opacity="0.10"/>
|
||||||
|
<stop offset="100%" stop-color="var(--ins-warning)" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<line v-for="y in [40,80,120]" :key="y" x1="0" :y1="y" x2="500" :y2="y" stroke="var(--border-light)" stroke-width="0.5"/>
|
||||||
|
<path class="opacity-[0.08]" d="M20,80 L60,72 L100,75 L140,60 L180,52 L220,48 L260,44 L300,40 L340,36 L380,30 L420,28 L460,22 L460,140 L20,140Z" fill="url(#revGrad)"/>
|
||||||
|
<path d="M20,80 L60,72 L100,75 L140,60 L180,52 L220,48 L260,44 L300,40 L340,36 L380,30 L420,28 L460,22" fill="none" stroke="var(--ins-indigo)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<circle cx="460" cy="22" r="3" fill="var(--ins-indigo)" stroke="#fff" stroke-width="2"/>
|
||||||
|
<path class="opacity-[0.08]" d="M20,115 L60,112 L100,110 L140,108 L180,106 L220,102 L260,100 L300,96 L340,94 L380,92 L420,88 L460,84 L460,140 L20,140Z" fill="url(#taxGrad2)"/>
|
||||||
|
<path d="M20,115 L60,112 L100,110 L140,108 L180,106 L220,102 L260,100 L300,96 L340,94 L380,92 L420,88 L460,84" fill="none" stroke="var(--ins-warning)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<circle cx="460" cy="84" r="3" fill="var(--ins-warning)" stroke="#fff" stroke-width="2"/>
|
||||||
|
<text v-for="(m, i) in chartMonths" :key="i" class="text-[9px] fill-[var(--ins-text-tertiary)] font-manrope font-semibold" :x="20 + i * 40" y="155">{{ m }}</text>
|
||||||
|
<circle cx="180" cy="10" r="4" fill="var(--ins-indigo)"/><text class="text-[9px] fill-[var(--ins-text-tertiary)] font-manrope font-semibold" x="188" y="13">Выручка</text>
|
||||||
|
<circle cx="250" cy="10" r="4" fill="var(--ins-warning)"/><text class="text-[9px] fill-[var(--ins-text-tertiary)] font-manrope font-semibold" x="258" y="13">Налоги</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="glass rounded-xl flex flex-col items-center justify-center">
|
||||||
|
<div class="text-[13px] font-bold text-[var(--ins-text-primary)] mb-4 w-full px-5 flex items-center justify-between">
|
||||||
|
Health Score
|
||||||
|
<span class="text-[10px] font-semibold text-[var(--ins-text-tertiary)] px-2.5 py-[3px] bg-[var(--bg-base)] rounded-full">2025</span>
|
||||||
|
</div>
|
||||||
|
<HealthDonut :score="D.healthScore" :spinning="loading" @attention="onOpenDetail('pulse', 'attention')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 3: Tax Structure + Deadlines -->
|
||||||
|
<div class="grid grid-cols-2 gap-3 mb-3.5">
|
||||||
|
<div class="glass rounded-xl p-5">
|
||||||
|
<div class="text-[13px] font-bold text-[var(--ins-text-primary)] mb-4 flex items-center justify-between">
|
||||||
|
Налоговая структура
|
||||||
|
<span class="text-[10px] font-semibold text-[var(--ins-text-tertiary)] px-2.5 py-[3px] bg-[var(--bg-base)] rounded-full">по видам</span>
|
||||||
|
</div>
|
||||||
|
<TaxStructureBars :bars="taxBars" />
|
||||||
|
</div>
|
||||||
|
<div class="glass rounded-xl p-5">
|
||||||
|
<div class="text-[13px] font-bold text-[var(--ins-text-primary)] mb-4 flex items-center justify-between">
|
||||||
|
Ближайшие дедлайны
|
||||||
|
<span class="text-[10px] font-semibold text-[var(--ins-text-tertiary)] px-2.5 py-[3px] bg-[var(--bg-base)] rounded-full">45 дней</span>
|
||||||
|
</div>
|
||||||
|
<DeadlineTimeline :items="D.deadlines" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 4: Declarations + Monthly Chart -->
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div class="glass rounded-xl p-5">
|
||||||
|
<div class="text-[13px] font-bold text-[var(--ins-text-primary)] mb-4 flex items-center justify-between">
|
||||||
|
Статус деклараций
|
||||||
|
<span class="text-[10px] font-semibold text-[var(--ins-text-tertiary)] px-2.5 py-[3px] bg-[var(--bg-base)] rounded-full">2025</span>
|
||||||
|
</div>
|
||||||
|
<DeclarationBars :items="D.declarationStatus" />
|
||||||
|
</div>
|
||||||
|
<div class="glass rounded-xl p-5">
|
||||||
|
<div class="text-[13px] font-bold text-[var(--ins-text-primary)] mb-4 flex items-center justify-between">
|
||||||
|
Налоги по месяцам
|
||||||
|
<span class="text-[10px] font-semibold text-[var(--ins-text-tertiary)] px-2.5 py-[3px] bg-[var(--bg-base)] rounded-full">₼ тыс.</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-end gap-1 h-[120px] pb-5 relative">
|
||||||
|
<div class="absolute bottom-5 left-0 right-0 h-px bg-[var(--border-light)]" />
|
||||||
|
<div v-for="(m, i) in chartMonths" :key="i" class="flex-1 flex flex-col items-center gap-[2px]">
|
||||||
|
<div class="w-full rounded-t-[3px] transition-all duration-300 min-h-[2px]" :style="{ height: barHeight(D.charts.taxes[i], 70) + '%', background: 'var(--ins-indigo)' }" />
|
||||||
|
<div class="w-full rounded-t-[3px] transition-all duration-300 min-h-[2px] opacity-50" :style="{ height: barHeight(D.charts.taxesPaid[i], 70) + '%', background: 'var(--ins-warning)' }" />
|
||||||
|
<div class="text-[8px] text-[var(--ins-text-tertiary)] font-semibold">{{ m }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3 text-[10px] font-bold mt-1">
|
||||||
|
<span class="text-[var(--ins-indigo)]">● Начислено</span>
|
||||||
|
<span class="text-[var(--ins-warning)]">● Уплачено</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Detail panel -->
|
||||||
|
<DetailPanel
|
||||||
|
:open="detailOpen"
|
||||||
|
:title="detailData?.title || ''"
|
||||||
|
:badge="detailData?.badge || ''"
|
||||||
|
:columns="detailData?.head || []"
|
||||||
|
:rows="detailData?.rows || []"
|
||||||
|
:summary="detailData?.sum || ''"
|
||||||
|
@close="closeDetail"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed, watch, onMounted, onUnmounted } from "vue"
|
||||||
|
import PulseKpiCircle from "@/components/pulse/PulseKpiCircle.vue"
|
||||||
|
import HealthDonut from "@/components/pulse/HealthDonut.vue"
|
||||||
|
import TaxStructureBars from "@/components/pulse/TaxStructureBars.vue"
|
||||||
|
import DeadlineTimeline from "@/components/pulse/DeadlineTimeline.vue"
|
||||||
|
import DeclarationBars from "@/components/pulse/DeclarationBars.vue"
|
||||||
|
import DetailPanel from "@/components/shared/DetailPanel.vue"
|
||||||
|
import { useStore } from "@/data/store"
|
||||||
|
import { useContentSwap } from "@/utils/useContentSwap"
|
||||||
|
|
||||||
|
const { D } = useStore()
|
||||||
|
const { cardsClass, detailOpen, openDetail, closeDetail } = useContentSwap()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const inspected = ref(false)
|
||||||
|
const popoverOpen = ref(false)
|
||||||
|
const detailData = ref(null)
|
||||||
|
|
||||||
|
const monthLabels = ["Yan", "Fev", "Mar", "Apr", "May", "İyn", "İyl", "Avq", "Sen", "Okt", "Noy", "Dek"]
|
||||||
|
const chartMonths = ["Янв", "Фев", "Мар", "Апр", "Май", "Июн", "Июл", "Авг", "Сен", "Окт", "Ноя", "Дек"]
|
||||||
|
|
||||||
|
const period = reactive({
|
||||||
|
year: "2025",
|
||||||
|
quarters: ["Q4"],
|
||||||
|
months: [9, 10, 11], // 0-indexed month indices
|
||||||
|
})
|
||||||
|
|
||||||
|
// Quarter → month mapping (0-indexed)
|
||||||
|
const qMonths = { Q1: [0,1,2], Q2: [3,4,5], Q3: [6,7,8], Q4: [9,10,11] }
|
||||||
|
|
||||||
|
function toggleQ(q) {
|
||||||
|
const idx = period.quarters.indexOf(q)
|
||||||
|
if (idx >= 0) {
|
||||||
|
period.quarters.splice(idx, 1)
|
||||||
|
} else {
|
||||||
|
period.quarters.push(q)
|
||||||
|
}
|
||||||
|
syncMonthsFromQuarters()
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncMonthsFromQuarters() {
|
||||||
|
const active = new Set()
|
||||||
|
for (const q of period.quarters) {
|
||||||
|
for (const m of qMonths[q]) active.add(m)
|
||||||
|
}
|
||||||
|
period.months = [...active].sort((a, b) => a - b)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMonth(mi) {
|
||||||
|
const idx = period.months.indexOf(mi)
|
||||||
|
if (idx >= 0) period.months.splice(idx, 1)
|
||||||
|
else period.months.push(mi)
|
||||||
|
period.months.sort((a, b) => a - b)
|
||||||
|
syncQuartersFromMonths()
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncQuartersFromMonths() {
|
||||||
|
const newQ = []
|
||||||
|
for (const [q, ms] of Object.entries(qMonths)) {
|
||||||
|
if (ms.every(m => period.months.includes(m))) newQ.push(q)
|
||||||
|
}
|
||||||
|
period.quarters = newQ
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMonthActive(mi) {
|
||||||
|
return period.months.includes(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
const kpis = computed(() => [
|
||||||
|
{ key: "revenue", label: "Выручка", value: D.kpi.revenue.value, change: D.kpi.revenue.change, up: true, color: "green" },
|
||||||
|
{ key: "taxload", label: "Нал. нагрузка", value: D.kpi.taxload.value, change: D.kpi.taxload.change, up: false, color: "yellow" },
|
||||||
|
{ key: "employees", label: "Сотрудники", value: D.kpi.employees.value, change: D.kpi.employees.change, up: true, color: "blue" },
|
||||||
|
{ key: "declarations", label: "Декларации", value: D.kpi.declarations.value, change: D.kpi.declarations.change, up: false, color: "yellow" },
|
||||||
|
{ key: "inspections", label: "Проверки", value: D.kpi.inspections.value, change: D.kpi.inspections.change, up: false, color: "red", valueStyle: "color: var(--ins-warning)" },
|
||||||
|
])
|
||||||
|
|
||||||
|
const taxBars = computed(() =>
|
||||||
|
D.taxStructure.map((t) => ({
|
||||||
|
name: t.name, value: t.value, pct: t.pct, color: t.color,
|
||||||
|
widthPct: (t.pct / 31.5) * 82,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const dateRange = computed(() => {
|
||||||
|
if (period.months.length === 0) return `01.01.${period.year} – 31.12.${period.year}`
|
||||||
|
const first = period.months[0]
|
||||||
|
const last = period.months[period.months.length - 1]
|
||||||
|
const fDay = String(first + 1).padStart(2, "0")
|
||||||
|
const lastDays = [31,28,31,30,31,30,31,31,30,31,30,31]
|
||||||
|
const lDay = lastDays[last]
|
||||||
|
const lMonth = String(last + 1).padStart(2, "0")
|
||||||
|
return `01.${fDay}.${period.year} – ${lDay}.${lMonth}.${period.year}`
|
||||||
|
})
|
||||||
|
|
||||||
|
function doInspect() {
|
||||||
|
popoverOpen.value = false
|
||||||
|
loading.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
loading.value = false
|
||||||
|
inspected.value = true
|
||||||
|
}, 1200)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOpenDetail(ctx, key) {
|
||||||
|
detailData.value = D.pulse[key] || D[ctx]?.[key] || null
|
||||||
|
if (detailData.value) openDetail()
|
||||||
|
}
|
||||||
|
|
||||||
|
function barHeight(val, max) {
|
||||||
|
return (val / max) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClickOutside(e) {
|
||||||
|
if (popoverOpen.value) popoverOpen.value = false
|
||||||
|
}
|
||||||
|
onMounted(() => document.addEventListener("click", handleClickOutside))
|
||||||
|
onUnmounted(() => document.removeEventListener("click", handleClickOutside))
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
<template>
|
||||||
|
<div class="animate-fade-slide-in">
|
||||||
|
<FilterBar
|
||||||
|
title="КАТЕГОРИИ"
|
||||||
|
:groups="filterGroups"
|
||||||
|
v-model="filters"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="content-swap mb-5" :class="cardsClass">
|
||||||
|
<div class="grid grid-cols-4 gap-3">
|
||||||
|
<KpiCard
|
||||||
|
v-for="card in visibleCards"
|
||||||
|
:key="card.key"
|
||||||
|
v-bind="card"
|
||||||
|
:change-up="true"
|
||||||
|
:small-value="true"
|
||||||
|
@click="onCardClick(card.key)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DetailPanel
|
||||||
|
:open="detailOpen"
|
||||||
|
:title="detailData?.title || ''"
|
||||||
|
:columns="detailData?.head || []"
|
||||||
|
:rows="detailData?.rows || []"
|
||||||
|
:summary="detailData?.sum || ''"
|
||||||
|
@close="closeDetail"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from "vue"
|
||||||
|
import FilterBar from "@/components/shared/FilterBar.vue"
|
||||||
|
import KpiCard from "@/components/shared/KpiCard.vue"
|
||||||
|
import DetailPanel from "@/components/shared/DetailPanel.vue"
|
||||||
|
import { useStore } from "@/data/store"
|
||||||
|
import { useContentSwap } from "@/utils/useContentSwap"
|
||||||
|
|
||||||
|
const { D } = useStore()
|
||||||
|
const { cardsClass, detailOpen, openDetail, closeDetail } = useContentSwap()
|
||||||
|
|
||||||
|
const filters = ref({})
|
||||||
|
const detailData = ref(null)
|
||||||
|
|
||||||
|
const filterGroups = [
|
||||||
|
{
|
||||||
|
chips: [
|
||||||
|
{ key: "rates", label: "Ставки" },
|
||||||
|
{ key: "social", label: "Взносы" },
|
||||||
|
{ key: "limits", label: "Лимиты" },
|
||||||
|
{ key: "inspection", label: "Инспекция" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const allCards = [
|
||||||
|
{ key: "rates", label: "Налоговые ставки", value: "ВКРП", subtitle: "9 налогов · ставки 2026", change: "📊", status: "normal" },
|
||||||
|
{ key: "social", label: "Соц. взносы", value: "DSMF·İTS", subtitle: "3 вида · работодатель + работник", change: "💰", status: "normal" },
|
||||||
|
{ key: "limits", label: "Пороги и лимиты", value: "₼400", subtitle: "МЗП · ƏDV порог · льготы", change: "📋", status: "normal" },
|
||||||
|
{ key: "inspection", label: "Инспекция", value: "Risk", subtitle: "пороги · дедлайны · частота", change: "🔍", status: "normal" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const visibleCards = computed(() =>
|
||||||
|
allCards.filter((c) => filters.value[c.key] !== false)
|
||||||
|
)
|
||||||
|
|
||||||
|
function onCardClick(key) {
|
||||||
|
detailData.value = D.region[key] || null
|
||||||
|
if (detailData.value) openDetail()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
<template>
|
||||||
|
<div class="animate-fade-slide-in">
|
||||||
|
<FilterBar
|
||||||
|
title="ВКРП"
|
||||||
|
:groups="filterGroups"
|
||||||
|
v-model="filters"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="content-swap mb-5" :class="cardsClass">
|
||||||
|
<div
|
||||||
|
class="grid gap-3"
|
||||||
|
style="grid-template-columns: repeat(auto-fill, minmax(165px, 1fr))"
|
||||||
|
>
|
||||||
|
<KpiCard
|
||||||
|
v-for="card in visibleCards"
|
||||||
|
:key="card.key"
|
||||||
|
v-bind="card"
|
||||||
|
@click="onCardClick(card.key)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DetailPanel
|
||||||
|
:open="detailOpen"
|
||||||
|
:title="detailData?.title || ''"
|
||||||
|
:badge="detailData?.badge || ''"
|
||||||
|
:columns="detailData?.head || []"
|
||||||
|
:rows="detailData?.rows || []"
|
||||||
|
:summary="detailData?.sum || ''"
|
||||||
|
@close="closeDetail"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from "vue"
|
||||||
|
import FilterBar from "@/components/shared/FilterBar.vue"
|
||||||
|
import KpiCard from "@/components/shared/KpiCard.vue"
|
||||||
|
import DetailPanel from "@/components/shared/DetailPanel.vue"
|
||||||
|
import { useStore } from "@/data/store"
|
||||||
|
import { useContentSwap } from "@/utils/useContentSwap"
|
||||||
|
|
||||||
|
const { D } = useStore()
|
||||||
|
const { cardsClass, detailOpen, openDetail, closeDetail } = useContentSwap()
|
||||||
|
|
||||||
|
const filters = ref({})
|
||||||
|
const detailData = ref(null)
|
||||||
|
|
||||||
|
const filterGroups = [
|
||||||
|
{
|
||||||
|
chips: [
|
||||||
|
{ key: "edv", label: "ƏDV" },
|
||||||
|
{ key: "menfeet", label: "Mənfəət" },
|
||||||
|
{ key: "gelir", label: "Gəlir" },
|
||||||
|
{ key: "sadelesdirilmis", label: "Sadələşd." },
|
||||||
|
{ key: "aksiz", label: "Aksiz" },
|
||||||
|
{ key: "yol", label: "Yol" },
|
||||||
|
{ key: "meden", label: "Mədən" },
|
||||||
|
{ key: "emlak", label: "Əmlak" },
|
||||||
|
{ key: "torpaq", label: "Torpaq" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
separator: true, label: "Соц.",
|
||||||
|
chips: [
|
||||||
|
{ key: "dsmf", label: "DSMF" },
|
||||||
|
{ key: "its", label: "İTS" },
|
||||||
|
{ key: "issizlik", label: "İşsizlik" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
separator: true, label: "Удерж.",
|
||||||
|
chips: [{ key: "menbede", label: "Mənbədə" }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const allCards = [
|
||||||
|
{ key: "edv", label: "ƏDV (НДС)", value: "₼5.8M", subtitle: "68 проверок", change: "+15%", changeUp: true, status: "normal" },
|
||||||
|
{ key: "menfeet", label: "Mənfəət vergisi", value: "₼4.2M", subtitle: "52 проверки", change: "+9%", changeUp: true, status: "normal" },
|
||||||
|
{ key: "gelir", label: "Gəlir vergisi", value: "₼3.1M", subtitle: "41 проверка", change: "-3%", changeUp: false, status: "normal" },
|
||||||
|
{ key: "sadelesdirilmis", label: "Sadələşdirilmiş", value: "₼0.5M", subtitle: "12 пров.", change: "+4%", changeUp: true, status: "normal" },
|
||||||
|
{ key: "aksiz", label: "Aksiz", value: "₼0.6M", subtitle: "8 пров.", change: "-7%", changeUp: false, status: "normal" },
|
||||||
|
{ key: "yol", label: "Yol", value: "₼0.2M", subtitle: "5 пров.", change: "+2%", changeUp: true, status: "normal" },
|
||||||
|
{ key: "meden", label: "Mədən", value: "₼0.1M", subtitle: "3 пров.", change: "+1%", changeUp: true, status: "normal" },
|
||||||
|
{ key: "emlak", label: "Əmlak", value: "₼0.4M", subtitle: "9 пров.", change: "-2%", changeUp: false, status: "normal" },
|
||||||
|
{ key: "torpaq", label: "Torpaq", value: "₼0.3M", subtitle: "6 пров.", change: "+3%", changeUp: true, status: "normal" },
|
||||||
|
{ key: "dsmf", label: "DSMF", value: "₼1.5M", subtitle: "28 пров.", change: "+6%", changeUp: true, status: "normal" },
|
||||||
|
{ key: "its", label: "İTS", value: "₼0.9M", subtitle: "18 пров.", change: "+5%", changeUp: true, status: "normal" },
|
||||||
|
{ key: "issizlik", label: "İşsizlik", value: "₼0.7M", subtitle: "14 пров.", change: "+3%", changeUp: true, status: "normal" },
|
||||||
|
{ key: "menbede", label: "Mənbədə tutulan", value: "₼1.9M", subtitle: "48 пров.", change: "+18%", changeUp: true, status: "critical" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const visibleCards = computed(() =>
|
||||||
|
allCards.filter((c) => filters.value[c.key] !== false)
|
||||||
|
)
|
||||||
|
|
||||||
|
function onCardClick(key) {
|
||||||
|
detailData.value = D.tax[key] || null
|
||||||
|
if (detailData.value) openDetail()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { createRouter, createWebHistory } from "vue-router"
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
redirect: "/pulse",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/pulse",
|
||||||
|
name: "Pulse",
|
||||||
|
component: () => import("@/pages/PulsePage.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/inspection",
|
||||||
|
name: "Inspection",
|
||||||
|
component: () => import("@/pages/InspectionPage.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/tax-types",
|
||||||
|
name: "TaxTypes",
|
||||||
|
component: () => import("@/pages/TaxTypesPage.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/accounting",
|
||||||
|
name: "Accounting",
|
||||||
|
component: () => import("@/pages/AccountingPage.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/settings",
|
||||||
|
name: "Settings",
|
||||||
|
component: () => import("@/pages/SettingsPage.vue"),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory("/inspector"),
|
||||||
|
routes,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { ref, nextTick } from "vue"
|
||||||
|
|
||||||
|
export function useContentSwap() {
|
||||||
|
const cardsClass = ref("")
|
||||||
|
const detailOpen = ref(false)
|
||||||
|
|
||||||
|
function openDetail() {
|
||||||
|
cardsClass.value = "fading-out"
|
||||||
|
setTimeout(() => {
|
||||||
|
cardsClass.value = "gone"
|
||||||
|
detailOpen.value = true
|
||||||
|
}, 350)
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDetail() {
|
||||||
|
// DetailPanel already played its leave animation (450ms)
|
||||||
|
// and emitted 'close'. Now bring cards back.
|
||||||
|
detailOpen.value = false
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
// Start invisible, shifted down
|
||||||
|
cardsClass.value = "fading-in"
|
||||||
|
|
||||||
|
// After browser renders invisible state, start reveal transition
|
||||||
|
setTimeout(() => {
|
||||||
|
cardsClass.value = "revealing"
|
||||||
|
}, 50)
|
||||||
|
|
||||||
|
// Clean up after transition
|
||||||
|
setTimeout(() => {
|
||||||
|
cardsClass.value = ""
|
||||||
|
}, 700)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { cardsClass, detailOpen, openDetail, closeDetail }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import frappeUIPreset from "frappe-ui/src/tailwind/preset"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
presets: [frappeUIPreset],
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||||
|
"./node_modules/frappe-ui/src/components/**/*.{vue,js,ts,jsx,tsx}",
|
||||||
|
"../node_modules/frappe-ui/src/components/**/*.{vue,js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
"ins-indigo": "#4f46e5",
|
||||||
|
"ins-positive": "#059669",
|
||||||
|
"ins-negative": "#dc2626",
|
||||||
|
"ins-warning": "#d97706",
|
||||||
|
"ins-info": "#2563eb",
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
manrope: ["Manrope", "sans-serif"],
|
||||||
|
mono: ["Source Code Pro", "monospace"],
|
||||||
|
},
|
||||||
|
maxWidth: {
|
||||||
|
content: "1480px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { defineConfig } from "vite"
|
||||||
|
import vue from "@vitejs/plugin-vue"
|
||||||
|
import frappeui from "frappe-ui/vite"
|
||||||
|
import path from "path"
|
||||||
|
import fs from "fs"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
port: 8081,
|
||||||
|
proxy: getProxyOptions(),
|
||||||
|
allowedHosts: true,
|
||||||
|
},
|
||||||
|
plugins: [vue(), frappeui()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: "../inspector/public/frontend",
|
||||||
|
emptyOutDir: true,
|
||||||
|
target: "es2015",
|
||||||
|
commonjsOptions: {
|
||||||
|
include: [/tailwind.config.js/, /node_modules/],
|
||||||
|
},
|
||||||
|
sourcemap: true,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
"frappe-ui": ["frappe-ui"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
include: [
|
||||||
|
"frappe-ui > feather-icons",
|
||||||
|
"showdown",
|
||||||
|
"tailwind.config.js",
|
||||||
|
"engine.io-client",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
function getProxyOptions() {
|
||||||
|
const config = getCommonSiteConfig()
|
||||||
|
const webserver_port = config ? config.webserver_port : 8000
|
||||||
|
if (!config) {
|
||||||
|
console.log("No common_site_config.json found, using default port 8000")
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"^/(app|login|api|assets|files|private)": {
|
||||||
|
target: `http://127.0.0.1:${webserver_port}`,
|
||||||
|
ws: true,
|
||||||
|
router: function (req) {
|
||||||
|
const site_name = req.headers.host.split(":")[0]
|
||||||
|
return `http://${site_name}:${webserver_port}`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCommonSiteConfig() {
|
||||||
|
let currentDir = path.resolve(".")
|
||||||
|
while (currentDir !== "/") {
|
||||||
|
if (
|
||||||
|
fs.existsSync(path.join(currentDir, "sites")) &&
|
||||||
|
fs.existsSync(path.join(currentDir, "apps"))
|
||||||
|
) {
|
||||||
|
let configPath = path.join(currentDir, "sites", "common_site_config.json")
|
||||||
|
if (fs.existsSync(configPath)) {
|
||||||
|
return JSON.parse(fs.readFileSync(configPath))
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
currentDir = path.resolve(currentDir, "..")
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
__version__ = "0.0.1"
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
app_name = "inspector"
|
||||||
|
app_title = "Inspector"
|
||||||
|
app_publisher = "Jey Soft LLC"
|
||||||
|
app_description = "Inspector - Tax Audit Dashboard"
|
||||||
|
app_email = "inspector@jeysoft.az"
|
||||||
|
app_license = "mit"
|
||||||
|
|
||||||
|
required_apps = ["frappe"]
|
||||||
|
|
||||||
|
app_logo_url = "/assets/inspector/images/inspector-logo.svg"
|
||||||
|
|
||||||
|
add_to_apps_screen = [
|
||||||
|
{
|
||||||
|
"name": "inspector",
|
||||||
|
"logo": "/assets/inspector/images/inspector-logo.svg",
|
||||||
|
"title": "Inspector",
|
||||||
|
"route": "/inspector",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
website_route_rules = [
|
||||||
|
{"from_route": "/inspector/<path:app_path>", "to_route": "inspector"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"autoname": "format:{tax_type}-{frequency}",
|
||||||
|
"creation": "2026-03-30 00:00:00.000000",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"tax_type",
|
||||||
|
"frequency",
|
||||||
|
"day_of_month",
|
||||||
|
"description"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "tax_type",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Tax Type",
|
||||||
|
"options": "\nÆDV\nMÉnfÉÉt\nGÉlir\nSadÉlÉÅdirilmiÅ\nAksiz\nYol\nMÉdÉn\nÆmlak\nTorpaq\nDSMF\nİTS\nİÅsizlik\nMÉnbÉdÉ",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "frequency",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Frequency",
|
||||||
|
"options": "Monthly\nQuarterly\nAnnual",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "day_of_month",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Day of Month",
|
||||||
|
"description": "Day of month when payment/filing is due"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "description",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Description"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 0,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2026-03-30 00:00:00.000000",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Inspector",
|
||||||
|
"name": "Tax Deadline",
|
||||||
|
"naming_rule": "Expression",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "creation",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
|
"title_field": "tax_type"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class TaxDeadline(Document):
|
||||||
|
pass
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"autoname": "format:DEC-{#####}",
|
||||||
|
"creation": "2026-03-30 00:00:00.000000",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"company",
|
||||||
|
"tax_type",
|
||||||
|
"period",
|
||||||
|
"column_break_1",
|
||||||
|
"filing_date",
|
||||||
|
"due_date",
|
||||||
|
"status",
|
||||||
|
"section_break_1",
|
||||||
|
"amount"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "company",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Company",
|
||||||
|
"options": "Company",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "tax_type",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Tax Type",
|
||||||
|
"options": "\nÆDV\nMÉnfÉÉt\nGÉlir\nSadÉlÉÅdirilmiÅ\nAksiz\nYol\nMÉdÉn\nÆmlak\nTorpaq\nDSMF\nİTS\nİÅsizlik\nMÉnbÉdÉ",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "period",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Period",
|
||||||
|
"description": "e.g. 2025-Q1, 2025-03"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_1",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "filing_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "Filing Date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "due_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Due Date",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "status",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Status",
|
||||||
|
"options": "Draft\nFiled\nAccepted\nLate",
|
||||||
|
"default": "Draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_1",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Amount"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 0,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2026-03-30 00:00:00.000000",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Inspector",
|
||||||
|
"name": "Tax Declaration",
|
||||||
|
"naming_rule": "Expression",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "creation",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
|
"title_field": "tax_type"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class TaxDeclaration(Document):
|
||||||
|
pass
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"autoname": "format:INS-{#####}",
|
||||||
|
"creation": "2026-03-30 00:00:00.000000",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"company",
|
||||||
|
"tax_type",
|
||||||
|
"voen",
|
||||||
|
"column_break_1",
|
||||||
|
"date_opened",
|
||||||
|
"date_closed",
|
||||||
|
"status",
|
||||||
|
"section_break_1",
|
||||||
|
"risk_score",
|
||||||
|
"penalty_amount",
|
||||||
|
"saved_amount",
|
||||||
|
"section_break_2",
|
||||||
|
"notes"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "company",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Company",
|
||||||
|
"options": "Company",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "tax_type",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Tax Type",
|
||||||
|
"options": "\nÆDV\nMÉnfÉÉt\nGÉlir\nSadÉlÉÅdirilmiÅ\nAksiz\nYol\nMÉdÉn\nÆmlak\nTorpaq\nDSMF\nİTS\nİÅsizlik\nMÉnbÉdÉ",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "voen",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "VÖEN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_1",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "date_opened",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Date Opened",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "date_closed",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "Date Closed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "status",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Status",
|
||||||
|
"options": "Open\nIn Progress\nClosed",
|
||||||
|
"default": "Open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_1",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Scores & Amounts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "risk_score",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Risk Score",
|
||||||
|
"description": "0-100"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "penalty_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Penalty Amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "saved_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Saved Amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_2",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Notes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "notes",
|
||||||
|
"fieldtype": "Text",
|
||||||
|
"label": "Notes"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 0,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2026-03-30 00:00:00.000000",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Inspector",
|
||||||
|
"name": "Tax Inspection",
|
||||||
|
"naming_rule": "Expression",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "creation",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
|
"title_field": "tax_type"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class TaxInspection(Document):
|
||||||
|
pass
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"autoname": "format:{company}-{setting_key}",
|
||||||
|
"creation": "2026-03-30 00:00:00.000000",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"company",
|
||||||
|
"category",
|
||||||
|
"setting_key",
|
||||||
|
"column_break_1",
|
||||||
|
"setting_value",
|
||||||
|
"legal_reference",
|
||||||
|
"effective_date"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "company",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Company",
|
||||||
|
"options": "Company",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "category",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Category",
|
||||||
|
"options": "rates\nsocial\nlimits\ninspection",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "setting_key",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Setting Key",
|
||||||
|
"reqd": 1,
|
||||||
|
"unique": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_1",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "setting_value",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "legal_reference",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Legal Reference"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "effective_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "Effective Date"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 0,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2026-03-30 00:00:00.000000",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Inspector",
|
||||||
|
"name": "Tax Setting",
|
||||||
|
"naming_rule": "Expression",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "creation",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
|
"title_field": "setting_key"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class TaxSetting(Document):
|
||||||
|
pass
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Inspector
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
[pre_model_sync]
|
||||||
|
# Patches added in this section will be executed before doctypes are migrated
|
||||||
|
# Read docs to understand patches: https://frappeframework.com/docs/v14/user/en/database-migrations
|
||||||
|
|
||||||
|
[post_model_sync]
|
||||||
|
# Patches added in this section will be executed after doctypes are migrated
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="64" height="64" rx="14" fill="url(#g)"/>
|
||||||
|
<text x="32" y="38" text-anchor="middle" font-family="system-ui,-apple-system,sans-serif" font-weight="800" font-size="22" fill="#fff">JI</text>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="g" x1="0" y1="0" x2="64" y2="64" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#4f46e5"/>
|
||||||
|
<stop offset="1" stop-color="#7c3aed"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 517 B |
|
|
@ -0,0 +1,29 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Inspector</title>
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Manrope:wght@300;400;500;600;700;800&family=Source+Code+Pro:wght@400;500;600;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<script type="module" crossorigin src="/assets/inspector/frontend/assets/index-DUszJK-2.js"></script>
|
||||||
|
<link rel="modulepreload" crossorigin href="/assets/inspector/frontend/assets/frappe-ui-D3iQUq0I.js">
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/inspector/frontend/assets/frappe-ui-D1rk2b8Q.css">
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/inspector/frontend/assets/index-B1mkOpgm.css">
|
||||||
|
</head>
|
||||||
|
<body class="antialiased">
|
||||||
|
<div id="app"></div>
|
||||||
|
<div id="modals"></div>
|
||||||
|
<div id="popovers"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.csrf_token = "{{ csrf_token }}"
|
||||||
|
window.site_name = "{{ site_name }}"
|
||||||
|
if (!window.frappe) window.frappe = {}
|
||||||
|
frappe.boot = {{ boot }}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
no_cache = 1
|
||||||
|
|
||||||
|
|
||||||
|
def get_context(context):
|
||||||
|
csrf_token = frappe.sessions.get_csrf_token()
|
||||||
|
frappe.db.commit() # nosemgrep
|
||||||
|
context = frappe._dict()
|
||||||
|
context.csrf_token = csrf_token
|
||||||
|
context.boot = get_boot()
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist(methods=["POST"], allow_guest=True)
|
||||||
|
def get_context_for_dev():
|
||||||
|
if not frappe.conf.developer_mode:
|
||||||
|
frappe.throw(frappe._("This method is only meant for developer mode"))
|
||||||
|
return get_boot()
|
||||||
|
|
||||||
|
|
||||||
|
def get_boot():
|
||||||
|
return frappe._dict(
|
||||||
|
{
|
||||||
|
"site_name": frappe.local.site,
|
||||||
|
"default_route": "/inspector",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) [year] [fullname]
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
[project]
|
||||||
|
name = "inspector"
|
||||||
|
authors = [
|
||||||
|
{ name = "Jey Soft LLC", email = "inspector@jeysoft.az"}
|
||||||
|
]
|
||||||
|
description = "Inspector - Tax Audit Dashboard"
|
||||||
|
requires-python = ">=3.14"
|
||||||
|
readme = "README.md"
|
||||||
|
dynamic = ["version"]
|
||||||
|
dependencies = [
|
||||||
|
# "frappe~=16.0.0" # Installed and managed by bench.
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["flit_core >=3.4,<4"]
|
||||||
|
build-backend = "flit_core.buildapi"
|
||||||
|
|
||||||
|
# These dependencies are only installed when developer mode is enabled
|
||||||
|
[tool.bench.dev-dependencies]
|
||||||
|
# package_name = "~=1.1.0"
|
||||||
|
|
||||||
|
# These apt dependencies will be installed from Ubuntu repositories when you host your app on Frappe Cloud
|
||||||
|
[deploy.dependencies.apt]
|
||||||
|
packages = []
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 110
|
||||||
|
target-version = "py314"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = [
|
||||||
|
"F",
|
||||||
|
"E",
|
||||||
|
"W",
|
||||||
|
"I",
|
||||||
|
"UP",
|
||||||
|
"B",
|
||||||
|
"RUF",
|
||||||
|
]
|
||||||
|
ignore = [
|
||||||
|
"B017", # assertRaises(Exception) - should be more specific
|
||||||
|
"B018", # useless expression, not assigned to anything
|
||||||
|
"B023", # function doesn't bind loop variable - will have last iteration's value
|
||||||
|
"B904", # raise inside except without from
|
||||||
|
"E101", # indentation contains mixed spaces and tabs
|
||||||
|
"E402", # module level import not at top of file
|
||||||
|
"E501", # line too long
|
||||||
|
"E741", # ambiguous variable name
|
||||||
|
"F401", # "unused" imports
|
||||||
|
"F403", # can't detect undefined names from * import
|
||||||
|
"F405", # can't detect undefined names from * import
|
||||||
|
"F722", # syntax error in forward type annotation
|
||||||
|
"W191", # indentation contains tabs
|
||||||
|
"UP030", # Use implicit references for positional format fields (translations)
|
||||||
|
"UP031", # Use format specifiers instead of percent format
|
||||||
|
"UP032", # Use f-string instead of `format` call (translations)
|
||||||
|
"UP037", # quoted annotations
|
||||||
|
"UP040", # Use type aliases instead of type annotations
|
||||||
|
]
|
||||||
|
typing-modules = ["frappe.types.DF"]
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
quote-style = "double"
|
||||||
|
indent-style = "tab"
|
||||||
|
docstring-code-format = true
|
||||||
Loading…
Reference in New Issue