This commit is contained in:
relaxed 2024-12-24 05:01:19 +05:00
commit 40cdd962ce
68 changed files with 18241 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
db

BIN
bun.lockb Executable file

Binary file not shown.

19
jsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

3298
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

35
package.json Normal file
View File

@ -0,0 +1,35 @@
{
"name": "wdb",
"version": "0.1.0",
"description": "",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
"tauri": "tauri"
},
"license": "MIT",
"dependencies": {
"@tauri-apps/api": "^2.1.1",
"@tauri-apps/plugin-opener": "^2.2.2",
"@tauri-apps/plugin-fs": "~2.0.4",
"@tauri-apps/plugin-shell": "^2.2.0",
"@tauri-apps/plugin-sql": "~2.0.2"
},
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.15.0",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"svelte": "^5.15.0",
"svelte-check": "^4.1.1",
"typescript": "~5.6.3",
"vite": "^6.0.5",
"@tauri-apps/cli": "^2.1.0",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.17"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

7
src-tauri/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

5768
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

26
src-tauri/Cargo.toml Normal file
View File

@ -0,0 +1,26 @@
[package]
name = "wdb"
version = "0.1.0"
description = "wdb ais"
authors = ["Relaxed"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "wdb_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri-plugin-shell = "2.0.0-rc"
tauri-plugin-sql = { version = "2.0.0-rc", features = ["sqlite"] }

3
src-tauri/build.rs Normal file
View File

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@ -0,0 +1,14 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"opener:default",
"shell:allow-open",
"sql:default",
"sql:allow-load",
"sql:allow-execute"
]
}

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","opener:default","shell:allow-open","sql:default","sql:allow-load","sql:allow-execute"]}}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

9
src-tauri/src/lib.rs Normal file
View File

@ -0,0 +1,9 @@
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_sql::Builder::new().build())
.plugin(tauri_plugin_shell::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

6
src-tauri/src/main.rs Normal file
View File

@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
sooome_lib::run()
}

35
src-tauri/tauri.conf.json Normal file
View File

@ -0,0 +1,35 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "wdb",
"version": "0.1.0",
"identifier": "ru.ogkod.wdb",
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "npm run build",
"frontendDist": "../build"
},
"app": {
"windows": [
{
"title": "wdb",
"width": 800,
"height": 600
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}

33
src/app.html Normal file
View File

@ -0,0 +1,33 @@
<!doctype html>
<html lang="en" class="h-full
bg-[var(--w-bg)] dark:bg-[var(--b-bg)]
text-black dark:text-[var(--b-text)] ">
<head>
<meta charset="utf-8" />
<!-- <link rel="icon" href="%sveltekit.assets%/favicon.png" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Tauri + SvelteKit App</title>
<script>
if (
localStorage.getItem("color-theme") === "dark" ||
(!("color-theme" in localStorage) &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
</script>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">
<div class="flex flex-col h-svh">
%sveltekit.body%
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,18 @@
<script>
export let value;
export let is_required = true;
</script>
{#if is_required}
<input
type="date"
required
bind:value
class="w-full p-2 mx-2
border-2 border-black dark:border-none
bg-white dark:bg-black
text-black dark:text-white"
/>
{:else}
<input type="date" bind:value class="w-full bg-black text-white p-2 mx-2" />
{/if}

View File

@ -0,0 +1,8 @@
<script>
export let name = "";
</script>
<div class="w-auto flex justify-start items-center my-1">
<h1>{name}</h1>
<slot />
</div>

View File

@ -0,0 +1,64 @@
<script>
export let value = 0;
export let is_float = false;
export let min = 0;
export let max = 999999999;
export let step = "0.1";
export let is_required = true;
</script>
{#if is_float && is_required}
<input
type="number"
{min}
{max}
{step}
{value}
required
on:input={(e) => (value = parseFloat(e.target.value))}
class="w-full p-2 mx-2
border-2 border-black dark:border-none
bg-white dark:bg-black
text-black dark:text-white"
/>
{:else if is_float}
<input
type="number"
{min}
{max}
{step}
{value}
on:input={(e) => (value = parseFloat(e.target.value))}
class="w-full p-2 mx-2
border-2 border-black dark:border-none
bg-white dark:bg-black
text-black dark:text-white"
/>
{/if}
{#if !is_float && is_required}
<input
type="number"
{min}
{max}
{value}
required
on:input={(e) => (value = Number(e.target.value))}
class="w-full p-2 mx-2
border-2 border-black dark:border-none
bg-white dark:bg-black
text-black dark:text-white"
/>
{:else if !is_float}
<input
type="number"
{min}
{max}
{value}
on:input={(e) => (value = Number(e.target.value))}
class="w-full p-2 mx-2
border-2 border-black dark:border-none
bg-white dark:bg-black
text-black dark:text-white"
/>
{/if}

View File

@ -0,0 +1,104 @@
<script>
import { onMount } from "svelte";
const isNumericInput = (event) => {
const key = event.keyCode;
// Allow number line
return (
(key >= 48 && key <= 57) ||
// Allow number pad
(key >= 96 && key <= 105)
);
};
const isModifierKey = (event) => {
const key = event.keyCode;
// Allow Shift, Home, End
return (
event.shiftKey === true ||
key === 35 ||
key === 36 ||
// Allow Backspace, Tab, Enter, Delete
key === 8 ||
key === 9 ||
key === 13 ||
key === 46 ||
// Allow left, up, right, down
(key > 36 && key < 41) ||
// Allow Ctrl/Command + A,C,V,X,Z
((event.ctrlKey === true || event.metaKey === true) &&
(key === 65 ||
key === 67 ||
key === 86 ||
key === 88 ||
key === 90))
);
};
const enforceFormat = (event) => {
// Input must be of a valid number format or a modifier key, and not longer than ten digits
if (!isNumericInput(event) && !isModifierKey(event)) {
event.preventDefault();
}
};
const formatToPhone = (event) => {
if (isModifierKey(event)) {
return;
}
// First ten digits of input only
let input = event.target.value.replace(/\D/g, "").substring(1, 15);
const areaCode = input.substring(0, 3);
const middle = input.substring(3, 6);
const last = input.substring(6, 8);
const last2 = input.substring(8, 10);
if (input.length >= 8) {
phone = `+7 (${areaCode}) ${middle} - ${last} - ${last2}`;
} else if (input.length > 6) {
phone = `+7 (${areaCode}) ${middle} - ${last}`;
} else if (input.length > 3) {
phone = `+7 (${areaCode}) ${middle}`;
} else if (input.length >= 0) {
phone = `+7 (${areaCode}`;
}
};
onMount(() => {
const inputElement = document.getElementById("phoneNumber");
inputElement.addEventListener("keydown", enforceFormat);
inputElement.addEventListener("keyup", formatToPhone);
});
const pattern = ".{22,}";
export let phone = "+7 ";
export let is_required = true;
</script>
{#if is_required}
<input
type="tel"
class="w-full p-2 mx-2
border-2 border-black dark:border-none
bg-white dark:bg-black
text-black dark:text-white"
placeholder="+7 (777) 777 - 77 - 77"
bind:value={phone}
{pattern}
required
id="phoneNumber"
/>
{:else}
<input
type="tel"
class="w-full p-2 mx-2
border-2 border-black dark:border-none
bg-white dark:bg-black
text-black dark:text-white"
placeholder="+7 (777) 777 - 77 - 77"
bind:value={phone}
{pattern}
id="phoneNumber"
/>
{/if}

View File

@ -0,0 +1,55 @@
<script>
export let value;
export let query;
export let is_required = true;
</script>
{#if is_required}
<select
required
bind:value
class="w-full p-2 mx-2
border-2 border-black dark:border-none
bg-white dark:bg-black
text-black dark:text-white
appearance-none"
>
{#await query then vtypes}
{#each vtypes as vtype}
{#if vtype.id === value}
<option selected value={vtype.id}>
<slot {vtype} />
</option>
{:else}
<option value={vtype.id}>
<slot {vtype} />
</option>
{/if}
{/each}
{/await}
</select>
{:else}
<select
bind:value
class="w-full p-2 mx-2
border-2 border-black dark:border-none
bg-white dark:bg-black
text-black dark:text-white
appearance-none"
>
{#await query then vtypes}
{#each vtypes as vtype}
{#if vtype.id === value}
<option selected value={vtype.id}>
<slot {vtype} />
</option>
{:else}
<option value={vtype.id}>
<slot {vtype} />
</option>
{/if}
{/each}
{/await}
</select>
{/if}

View File

@ -0,0 +1,55 @@
<script>
export let is_input = true;
export let value = "";
export let min_len = 0;
export let max_len = 100;
export let is_required = true;
</script>
{#if is_input && is_required}
<input
type="text"
minlength={min_len}
maxlength={max_len}
{value}
required
on:input={(e) => (value = e.target.value)}
class="w-full p-2 mx-2
border-2 border-black dark:border-none
bg-white dark:bg-black
text-black dark:text-white"
/>
{:else if is_input}
<input
type="text"
minlength={min_len}
maxlength={max_len}
{value}
on:input={(e) => (value = e.target.value)}
class="w-full p-2 mx-2
border-2 border-black dark:border-none
bg-white dark:bg-black
text-black dark:text-white"
/>
{/if}
{#if !is_input && is_required}
<textarea
required
{value}
on:input={(e) => (value = e.target.value)}
class="w-full p-2 mx-2
border-2 border-black dark:border-none
bg-white dark:bg-black
text-black dark:text-white"
></textarea>
{:else if !is_input}
<textarea
{value}
on:input={(e) => (value = e.target.value)}
class="w-full p-2 mx-2
border-2 border-black dark:border-none
bg-white dark:bg-black
text-black dark:text-white"
></textarea>
{/if}

View File

@ -0,0 +1,48 @@
<script>
import Button from "../ui/Button.svelte";
import Search from "../ui/Search.svelte";
export let is_searching = false;
/** @type {any} */
export let current_item;
export let search_query_result = "";
export let columns = ["id", "name"];
export let current_table;
export let on_add_click = () => {};
export let toggle_add_dialog = () => {};
export let joins = "";
/** @typedef SearchParam
* @property {string} name
* @property {boolean} is_number
*/
/** @type {Array<SearchParam>}*/
export let ex_params = [];
</script>
<div class="m-2 flex space-x-2">
<Button
is_visible={!is_searching}
on_click={() => {
on_add_click();
toggle_add_dialog();
}}
>
Добавить
</Button>
<Search
bind:is_searching
bind:current_item
bind:search_query_result
{joins}
{ex_params}
{columns}
{current_table}
></Search>
</div>
<slot {is_searching} {search_query_result} />

View File

@ -0,0 +1,35 @@
<script>
import Button from "../ui/Button.svelte";
export let is_dialog_item_add = false;
export let on_add = async () => {};
export let on_save = async () => {};
export let on_delete = async () => {};
export let form = null;
</script>
{#if is_dialog_item_add}
<Button
on_click={async () => {
if (form === null || form.checkValidity()) {
await on_add();
}
}}
>
Добавить
</Button>
{:else}
<div class="flex w-max space-x-2">
<Button
on_click={async () => {
if (form === null || form.checkValidity()) {
await on_save();
}
}}
>
Сохранить
</Button>
<Button on_click={on_delete}>Удалить</Button>
</div>
{/if}

View File

@ -0,0 +1,114 @@
<script>
import Dialog from "../ui/Dialog.svelte";
import TableCudButtons from "../shorts/TableCUDButtons.svelte";
import Button from "../ui/Button.svelte";
import Table from "../table/Table.svelte";
import Field from "../form/Field.svelte";
import TextInput from "../form/TextInput.svelte";
import Search from "../ui/Search.svelte";
export let is_item_dialog_open = false;
export let is_dialog_item_add = false;
export let cur_dialog_name = "__example__";
export let current_table;
/** @type {any} */
export let current_item;
export let query;
export let db;
export let toggle_add_dialog = () => {};
export let open_item_edit = (/** @type {any} */ item) => {};
export let sql_insert_short = async (
/** @type {any} */ arg1,
/** @type {any} */ arg2,
) => {};
export let sql_update_short = async (
/** @type {any} */ arg1,
/** @type {any} */ arg2,
) => {};
export let sql_delete_short = () => {};
export let second_col_name = "name";
export let columns = ["id", "name"];
export let columns_display = columns;
// export let is_searching = false;
export let is_searching = false;
let search_query_result = "";
</script>
<Dialog
bind:is_open={is_item_dialog_open}
on_close={() => {
is_dialog_item_add = false;
}}
name={cur_dialog_name}
let:form
>
{#if !is_dialog_item_add}
<h1>{columns_display[0]} {current_item.id}</h1>
{/if}
<Field name={columns_display[1]}>
<slot name="second_value_input">
<TextInput bind:value={current_item[second_col_name]}></TextInput>
</slot>
</Field>
<TableCudButtons
{form}
bind:is_dialog_item_add
on_add={async () => {
await sql_insert_short(current_table, [second_col_name]);
}}
on_save={async () => {
await sql_update_short(current_table, [second_col_name]);
}}
on_delete={sql_delete_short}
></TableCudButtons>
</Dialog>
<div class="m-2 flex space-x-2">
<Button
is_visible={!is_searching}
on_click={() => {
current_item = {
id: 0,
};
current_item[second_col_name] = "";
toggle_add_dialog();
}}
>
Добавить
</Button>
<Search
bind:is_searching
bind:current_item
bind:search_query_result
{columns}
{current_table}
></Search>
<!-- <Button -->
<!-- on_click={async () => { -->
<!-- await export_csv(data); -->
<!-- }} -->
<!-- > -->
<!-- CSV -->
<!-- </Button> -->
</div>
<Table
bind:is_searching
search_query={db.select(search_query_result)}
{open_item_edit}
{query}
{columns}
{columns_display}
></Table>

View File

@ -0,0 +1,20 @@
<script>
import Spinner from "../ui/Spinner.svelte";
import Table from "../table/Table.svelte";
import Tr from "../table/Tr.svelte";
import Atd from "../table/Atd.svelte";
export let query;
/** @type {Array<string>} */
export let columns = [];
/** @type {Array<string>} */
export let names = [];
</script>
<Table {columns} {query} let:item let:index>
<Tr {index}>
<Atd {item} {names}></Atd>
</Tr>
</Table>

View File

@ -0,0 +1,6 @@
import { writable } from "svelte/store";
export const theme_switch_is_resize = writable(false);
export const theme_switch_is_transparent = writable(false);
export const theme_switch_is_dark = writable(true);
export const theme_switch_is_update = writable(false);

View File

@ -0,0 +1,14 @@
<script>
import Td from "./Td.svelte";
/** @type {any} */
export let item = {};
/** @type {Array<string>} */
export let names = [];
</script>
{#each names as name}
<Td>
{item[name]}
</Td>
{/each}

View File

@ -0,0 +1,74 @@
<script>
import Atd from "./Atd.svelte";
import Tr from "./Tr.svelte";
import Spinner from "../ui/Spinner.svelte";
// export let data;
export let columns = ["__empty__"];
export let columns_display = columns;
export let class_name = "";
export let query;
export let open_item_edit = (/** @type {any} */ item) => {};
export let is_searching = false;
export let search_query;
</script>
{#if !is_searching}
{#await query}
<Spinner></Spinner>
{:then data}
<table class="w-max min-w-[200px] table-fixed {class_name}">
<tbody>
<tr
class="w-full sticky
bg-black text-white"
style="inset-block-start: 0;"
>
{#each columns_display as column_name}
<td class="w-max p-2 text-center">
{column_name}
</td>
{/each}
</tr>
{#each data as item, i}
<slot {item} index={i}>
<Tr on_click={() => open_item_edit(item)} index={i}>
<Atd {item} names={columns}></Atd>
</Tr>
</slot>
{/each}
</tbody>
</table>
{:catch}
<h1>Не удалось загрузить данные</h1>
{/await}
{:else if search_query !== ""}
{#await search_query}
<Spinner></Spinner>
{:then data}
<table class="w-max min-w-[200px] table-fixed {class_name}">
<tbody>
<tr
class="w-full sticky
bg-black text-white"
style="inset-block-start: 0;"
>
{#each columns_display as column_name}
<td class="w-max p-2 text-center">
{column_name}
</td>
{/each}
</tr>
{#each data as item, i}
<slot {item} index={i}>
<Tr on_click={() => open_item_edit(item)} index={i}>
<Atd {item} names={columns}></Atd>
</Tr>
</slot>
{/each}
</tbody>
</table>
{:catch}
<h1>Не удалось загрузить данные</h1>
{/await}
{/if}

View File

@ -0,0 +1,5 @@
<td
class="p-2 border-[1px] border-black overflow-hidden text-ellipsis break-words"
>
<slot />
</td>

View File

@ -0,0 +1,18 @@
<script>
export let index = 0;
export let on_click = () => {};
</script>
<!-- class="bg-white dark:bg-gray-800" -->
<!-- {index % 2 == 0 ? 'text-white' : 'text-black'} -->
<!-- {index % 2 == 0 ? 'bg-[#444]' : 'bg-[#dcdcdc]'}" -->
<tr
on:click={on_click}
class="
text-black dark:text-white
{index % 2 == 0
? 'bg-[#fff] dark:bg-gray-800'
: 'bg-[#e0e0e0] dark:bg-gray-700'}"
>
<slot />
</tr>

View File

@ -0,0 +1,35 @@
<script>
export let is_visible = true;
export let on_click = () => {};
export let on_submit = () => {};
/** @type {'primary' | 'regular' | 'second'} */
export let type = "primary";
export let class_name = "";
let styles = {
// bg-[#6664AF] dark:bg-[#6664AF] text-white text-2xl
primary: `w-max p-4 py-2 my-4 rounded-lg text-2xl
bg-[var(--w-button-bg)] dark:bg-[var(--b-button-bg)]
text-[var(--w-accent-text)] dark:text-[var(--b-accent-text)]
shadow-[2px_2px_#888] hover:bg-black hover:shadow-[4px_4px_#888]
transition-all duration-200 ${class_name}`,
regular: `w-max p-4 py-2 my-4 rounded-lg
bg-[#333] dark:bg-[#333] text-white text-2xl
shadow-none hover:bg-black hover:shadow-[4px_4px_#888]
transition-all duration-200 ${class_name}`,
second: `w-max p-4 py-2 my-4
border-2 border-black dark:border-[var(--b-border)]
bg-[#f0f0f0] dark:bg-[#303030] text-2xl
shadow-[-2px_-2px_#000] dark:shadow-[-2px_-2px_var(--b-border)] ${class_name}`,
};
</script>
{#if is_visible}
<button type="submit" class={styles[type]} on:click={on_click}>
<slot />
</button>
{/if}

View File

@ -0,0 +1,98 @@
<script>
// import ThemeSwitch from "./ThemeSwitch.svelte";
import x from "./x.svg";
export let is_open = false;
export let is_name_editable = false;
export let name = "";
export let class_name = "";
export let on_close = () => {
is_open = false;
};
let dialog;
let local_open = false;
function toggle_dialog(_) {
if (is_open && local_open) {
dialog.showModal();
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
is_open = false;
}
});
} else if (local_open === true) {
dialog.close();
document.removeEventListener(
"keydown",
(event) => {
if (event.key === "Escape") {
is_open = false;
}
},
true,
);
} else if (!local_open) {
local_open = true;
}
}
$: _ = toggle_dialog(is_open);
let form;
</script>
<dialog
bind:this={dialog}
class="top-0 left-0 w-full h-svh z-30
fixed overflow-hidden
max-w-full max-h-full
flex justify-center items-center
{is_open ? 'visible opacity-100' : 'invisible opacity-0'}
backdrop-blur-sm
bg-[#000000aa]
transition-all duration-300"
>
<div
class="w-[700px] h-[800px] mx-4 px-2 flex flex-col
border-[var(--w-border)] dark:border-[var(--b-border)] border-2 rounded-xl
bg-[var(--w-bg-second)] dark:bg-[var(--b-bg-second)]
text-[var(--w-text)] dark:text-[var(--b-text)] text-2xl
overflow-hidden
{class_name}"
>
<div class=" relative flex flex-initial justify-between items-center">
{#if is_name_editable}
<input
type="text"
bind:value={name}
class="bg-[var(--w-bg)] dark:bg-[var(--b-bg)] w-full focus-visible:outline-none"
/>
{:else}
<p class=" w-full focus-visible:outline-none">
{name}
</p>
{/if}
<button
on:click={() => {
is_open = false;
on_close();
}}
class="size-[60px] m-2 pb-3"
>
<img
src={x}
class="size-[40px] dark:invert"
alt="x close img"
/>
</button>
</div>
<form
bind:this={form}
action="/"
target="_self"
class="overflow-y-auto"
>
<slot {form}></slot>
</form>
</div>
</dialog>

View File

@ -0,0 +1,77 @@
<script>
import Button from "./Button.svelte";
let form;
let login = "";
let passoword = "";
export let is_logedin = false;
export let access_level = "";
export let check_login = async (l, p) => [false, ""];
let is_error = false;
</script>
{#if !is_logedin}
<div
class="w-full h-full pr-4 overflow-y-auto
flex justify-center items-center
bg-[var(--w-bg-second)] dark:bg-[#111]"
>
<form
bind:this={form}
class="flex flex-col justify-center items-center"
>
<h1 class="text-4xl">Вход</h1>
<input
type="text"
placeholder="login"
bind:value={login}
required
class="w-[400px] m-2 p-2
text-black dark:text-white
bg-white dark:bg-black"
/>
<input
type="password"
placeholder="password"
bind:value={passoword}
required
class="w-[400px] m-2 p-2
text-black dark:text-white
bg-white dark:bg-black"
/>
{#if is_error}
<h1>Неправильный логин или пароль</h1>
{/if}
<Button
class_name={"!w-[400px] !m-2 text-xl"}
on_click={async () => {
if (form.checkValidity()) {
if (login === "admin" && passoword === "1234") {
is_logedin = true;
access_level = "admin";
is_error = false;
login = "";
passoword = "";
} else {
const [is_ok, access_type] = await check_login(
login,
passoword,
);
if (!is_ok) {
is_error = true;
} else {
is_error = false;
login = "";
passoword = "";
is_logedin = true;
access_level = access_type;
}
}
}
}}
>
Войти
</Button>
</form>
</div>
{/if}

View File

@ -0,0 +1,57 @@
<script>
import ThemeSwitch from "./ThemeSwitch.svelte";
import Button from "./Button.svelte";
// /** @type {'tables' | 'viewes'} */
export let current_page = "";
/** @type {any} */
export let db_scheme = {};
export let is_view_open = false;
export let check_access = (name) => false;
export let is_logedin = false;
export let on_logout = () => {};
</script>
<div
class="w-full h-[80px] mb-2 pr-4 text-xl flex-initial
flex justify-between items-center
bg-[var(--w-bg-second)] dark:bg-[#111]"
>
<div class="flex">
{#if is_logedin}
{#each Object.entries(db_scheme) as page_name}
{#if check_access(page_name[0])}
<button
on:click={() => {
is_view_open = false;
current_page = page_name[0];
}}
class="m-4 mx-2 p-4
{current_page === page_name[0]
? 'bg-[#d8efff] dark:bg-[#114040]'
: 'bg-[var(--w-button-bg)] dark:bg-black text-[var(--w-accent-text)] dark:text-[var(--b-accent-text)]'}
border-2 border-[var(--w-border)] dark:border-[var(--b-border)]"
>
{page_name[0]}
</button>
{/if}
{/each}
{/if}
</div>
<div class="space-x-4 flex justify-center items-center">
<ThemeSwitch></ThemeSwitch>
{#if is_logedin}
<Button
on_click={on_logout}
class_name={"!text-black hover:!text-white dark:!text-white bg-[var(--w-red)] dark:bg-[var(--b-red)]"}
>
Выход
</Button>
{/if}
</div>
</div>

View File

@ -0,0 +1,192 @@
<script>
import Database from "@tauri-apps/plugin-sql";
import { onMount } from "svelte";
import NavBar from "$lib/components/ui/NavBar.svelte";
import SideMenu from "$lib/components/ui/SideMenu.svelte";
import LoginForm from "$lib/components/ui/LoginForm.svelte";
function toggle_update() {
is_loaded = false;
setTimeout(() => {
is_loaded = true;
});
}
function toggle_add_dialog() {
is_item_dialog_open = true;
is_dialog_item_add = true;
}
/** @param {Object} item */
function open_item_edit(item) {
is_item_dialog_open = true;
is_dialog_item_add = false;
current_item = structuredClone(item);
}
async function sql_delete_short() {
await db.execute(
`delete from ${current_table} where id = ${current_item.id}`,
);
is_item_dialog_open = false;
toggle_update();
}
/** @param {any} an */
function format_type_insert(an) {
if (typeof an === "string") {
return `"${an}"`;
} else if (an === null) {
return "NULL";
} else if (an === undefined) {
return null;
} else {
return `${an}`;
}
}
/**
* @param {string} table_name
* @param {Array<string>} columns
*/
async function sql_insert_short(table_name, columns) {
let q = `insert into ${table_name} (${columns.join(", ")}) values (${columns
.map((el) => format_type_insert(current_item[el]))
.filter((el) => el !== null)
.join(", ")})`;
console.log(current_item, q);
await db.execute(q);
is_item_dialog_open = false;
toggle_update();
}
/**
@param {string} name
@param {any} an
*/
function format_type_update(name, an) {
if (typeof an === "string") {
return `${name} = "${an}"`;
} else if (an === null) {
return `${name} = NULL`;
} else if (an === undefined) {
return null;
} else {
return `${name} = ${an}`;
}
}
/**
* @param {string} table_name
* @param {Array<string>} columns
*/
async function sql_update_short(table_name, columns) {
// let q = `UPDATE ${table_name} SET
// ${columns.map((el) => (typeof current_item[el] === "string" ? `${el} = "${current_item[el]}"` : `${el} = ${current_item[el]}`)).join(", ")}
// WHERE id = ${current_item.id}`;
let q = `UPDATE ${table_name} SET
${columns
.map((el) => format_type_update(el, current_item[el]))
.filter((el) => el !== null)
.join(", ")}
WHERE id = ${current_item.id}`;
// console.log(q);
await db.execute(q);
is_item_dialog_open = false;
toggle_update();
}
export let db_scheme;
/** @type {any} */
export let current_item = {};
/** @type {Database} */
export let db;
/** @type {string} */
let current_page = "Запросы";
let current_table = "";
let current_view = "";
let is_view_open = false;
export let is_item_dialog_open = false;
export let is_dialog_item_add = false;
let is_logedin = false;
export let data_access;
let access_level = "";
/**
* @param {string} access_level
* @param {string} tab_name
* @param {string | null} name
*/
export let check_access = (access_level, tab_name, name) => false;
/**
* @param {string} login
* @param {string} passowrd
*/
export let check_login = async (login, passowrd) => [false, ""];
let is_loaded = false;
export let load_db = async () => {};
onMount(async () => {
await load_db();
is_loaded = true;
});
$: cur_dialog_name = is_dialog_item_add
? `Добавить в ${db_scheme[current_page].tables[current_table]}`
: `Изменить ${db_scheme[current_page].tables[current_table]}`;
</script>
<NavBar
{is_logedin}
{db_scheme}
bind:current_page
bind:is_view_open
check_access={(name) => check_access(access_level, name, null)}
on_logout={() => {
is_logedin = false;
// @ts-ignore
access_level = "";
current_page = "Запросы";
current_table = "";
current_view = "";
}}
></NavBar>
<div class="w-full h-[calc(100vh-88px)] flex">
<SideMenu
{is_logedin}
bind:current_table
bind:current_view
bind:is_view_open
db_scheme={db_scheme[current_page]}
check_access={(name) =>
check_access(access_level, current_page, name)}
></SideMenu>
<div
class="w-full pr-4 overflow-y-auto rounded-tl-xl
bg-[var(--w-bg-second)] dark:bg-[#111]"
>
<LoginForm bind:is_logedin bind:access_level {check_login}
></LoginForm>
{#if is_loaded && is_logedin}
<slot
{current_page}
{current_table}
{current_view}
{is_view_open}
{cur_dialog_name}
{toggle_update}
{toggle_add_dialog}
{open_item_edit}
{sql_update_short}
{sql_insert_short}
{sql_delete_short}
/>
{/if}
</div>
</div>

View File

@ -0,0 +1,164 @@
<script>
import Button from "./Button.svelte";
import ViewTable from "../shorts/ViewTable.svelte";
export let is_searching = false;
export let search_value = "";
export let search_query_result = "";
/** @type {any} */
export let current_item;
export let columns = ["id", "name"];
export let current_table = "";
export let joins = "";
/** @typedef SearchParam
* @property {string} name
* @property {boolean} is_number
*/
/** @type {Array<SearchParam>}*/
export let ex_params = [];
export let is_contains = true;
/**
* @param {string} thing
* @returns {Number | NaN}
*/
function parse_number(thing) {
let num = NaN;
if (thing.includes(".")) {
num = parseFloat(thing);
} else {
num = Number(thing);
}
return num;
}
/**
* @param {string} param_name
* @param {string | null} value
* @param {string} sv
* @returns {string | null}
*/
function make_sql_param(param_name, value, sv) {
if (value !== null && typeof value === "string") {
if (is_contains) {
return `${param_name} LIKE "%${sv}%"`;
} else {
return `${param_name} LIKE "${sv}"`;
}
}
const num = parse_number(sv);
if (!isNaN(num)) {
return `${param_name} = ${sv}`;
}
return null;
}
function on_input(/** @type {any} */ _) {
/** @type {Array<string>} */
let params = [];
const search_purified = search_value.trim();
const search_splitted = search_purified.split(" ");
columns.forEach((el) => {
const param = make_sql_param(el, current_item[el], search_purified);
if (param !== null) {
params.push(param);
}
search_splitted.forEach((sp) => {
const param = make_sql_param(el, current_item[el], sp);
if (param !== null && !params.includes(param)) {
params.push(param);
}
});
});
if (ex_params.length !== 0) {
ex_params.forEach((el) => {
const param = make_sql_param(
el.name,
el.is_number ? null : "",
search_purified,
);
if (param !== null) {
params.push(param);
}
search_splitted.forEach((sp) => {
const param = make_sql_param(
el.name,
el.is_number ? null : "",
sp,
);
if (param !== null && !params.includes(param)) {
params.push(param);
}
});
});
}
if (params.length === 0) {
search_query_result = "";
} else {
search_query_result = `SELECT * FROM ${current_table} ${joins} WHERE ${params.join(" OR ")}`;
}
// console.log(`SELECT * FROM ${current_table} \n${joins} \nWHERE \n${params.join(" OR ")}`);
}
</script>
<div class="">
<div class=" flex justify-start items-center">
<Button
on_click={() => {
is_searching = !is_searching;
}}
type={is_searching ? "second" : "primary"}
>
{#if is_searching}
{"<-"}
{:else}
Поиск
{/if}
</Button>
{#if is_searching}
<input
type="text"
bind:value={search_value}
on:input={on_input}
class=" w-full mx-4 p-2 text-xl border-black border-2
bg-[#fff] dark:bg-gray-800
dark:text-white
"
/>
{/if}
</div>
{#if is_searching}
<div class="w-max flex justify-center items-center text-2xl">
<input
type="checkbox"
class="size-7 mr-4"
checked
on:click={() => {
is_contains = !is_contains;
on_input("");
}}
/> похоже?
</div>
{/if}
</div>

View File

@ -0,0 +1,63 @@
<script>
export let current_table = "";
export let current_view = "";
export let is_view_open = false;
export let db_scheme = { tables: {}, viewes: {} };
export let check_access = (name) => false;
export let is_logedin = false;
</script>
<div
class="w-[240px] h-full text-xl p-3 mr-2 flex-initial rounded-tr-xl
bg-[var(--w-bg-second)] dark:bg-[var(--b-bg-second)]"
>
{#if is_logedin}
{#if Object.entries(db_scheme.viewes).length !== 0 && Object.entries(db_scheme.viewes).some( (el) => check_access(el[0]), )}
<button
on:click={() => {
is_view_open = !is_view_open;
}}
class="py-2 my-1 w-full
{is_view_open
? 'bg-[#d8efff] dark:bg-[#114040]'
: 'bg-[var(--w-button-bg)] dark:bg-black text-[var(--w-accent-text)] dark:text-[var(--b-accent-text)]'}
border-2 border-[var(--w-border)] dark:border-[var(--b-border)]"
>
Отчеты
</button>
<div class="my-3 border-b-4 rounded-xl border-cyan-500"></div>
{/if}
{#each !is_view_open ? Object.entries(db_scheme.tables) : Object.entries(db_scheme.viewes) as item_name}
{#if check_access(item_name[0])}
<button
on:click={() => {
if (
`${current_table}` === item_name[0] ||
current_view === item_name[0]
) {
current_table = "";
current_view = "";
}
if (!is_view_open) {
current_table = item_name[0];
} else {
current_view = item_name[0];
}
}}
class="py-2 px-1 my-1 w-full
{current_table === item_name[0] || current_view === item_name[0]
? 'bg-[#d8efff] dark:bg-[#114040]'
: 'bg-[var(--w-button-bg)] dark:bg-black text-[var(--w-accent-text)] dark:text-[var(--b-accent-text)]'}
border-2 border-[var(--w-border)] dark:border-[var(--b-border)]"
>
{item_name[1]}
</button>
{/if}
{/each}
{/if}
</div>

View File

@ -0,0 +1,6 @@
<span
class="size-[40px] min-w-[40px]
border-[5px] border-[var(--w-border)] dark:border-[var(--b-border)] rounded-[50%]
inline-block box-border animate-spin"
style="border-bottom-color: transparent;"
></span>

View File

@ -0,0 +1,68 @@
<script>
import { onMount } from "svelte";
let is_resize = false;
let is_transparent = false;
let is_dark = true;
let is_update = false;
function handleSwitchDarkMode() {
setTimeout(() => {
is_transparent = true;
is_dark = !is_dark;
setTimeout(() => {
is_resize = false;
is_transparent = false;
setTimeout(() => {
is_update = true;
setTimeout(() => {
is_update = false;
}, 0);
}, 0);
}, 250);
}, 500);
is_resize = true;
const isDark = window.document.documentElement.classList.toggle("dark");
if (isDark) {
localStorage.setItem("color-theme", "dark");
} else {
localStorage.setItem("color-theme", "light");
}
}
onMount(() => {
if (localStorage.getItem("color-theme") === "light") {
is_dark = false;
}
});
</script>
{#if !is_update}
<div class="min-w-[40px] min-h-[40px] flex justify-center items-center">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="group min-h-[50px] min-w-[50px] rounded-full flex justify-center items-center
{is_dark ? 'bg-[var(--w-bg)]' : 'bg-[var(--b-bg)]'}
"
on:click={handleSwitchDarkMode}
>
<div
class=" rounded-full
{!is_transparent
? is_dark
? 'group-hover:bg-[var(--b-bg)] dark:group-hover:bg-[var(--b-bg)]'
: 'group-hover:bg-[var(--w-bg)] dark:group-hover:bg-[var(--w-bg)]'
: ''}
{!is_transparent
? is_resize
? 'h-[50px] w-[50px]'
: 'h-[25px] w-[25px]'
: 'h-[25px] w-[25px]'}
transition-all duration-500 ease-in-out"
></div>
</div>
</div>
{/if}

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path fill="#000000" d="M764.288 214.592 512 466.88 259.712 214.592a31.936 31.936 0 0 0-45.12 45.12L466.752 512 214.528 764.224a31.936 31.936 0 1 0 45.12 45.184L512 557.184l252.288 252.288a31.936 31.936 0 0 0 45.12-45.12L557.12 512.064l252.288-252.352a31.936 31.936 0 1 0-45.12-45.184z"/></svg>

After

Width:  |  Height:  |  Size: 507 B

52
src/lib/global.css Normal file
View File

@ -0,0 +1,52 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-family: JetBrainsMono;
src: url(/jbmv.ttf);
}
* {
-webkit-tap-highlight-color: transparent;
outline: none !important;
font-family: JetBrainsMono;
/* white theme */
/* --w-bg: #ddd; */
/* --w-bg-second: #ccc; */
/* --w-accent: #eee; */
/* --w-accent-text: #000; */
/* --w-text: #000; */
/* --w-border: #000; */
/* --w-button-bg: #fff; */
--w-bg: #fff;
--w-bg-second: #F4E8D3;
--w-accent: #67BA80;
--w-accent-text: #fff;
--w-text: #000;
--w-border: #000;
--w-button-bg: #67BA80;
--w-red: #fcc;
--w-green: #0f0;
--w-blue: #ccf;
--w-orange: #f80;
/* dark theme */
--b-bg: #333;
--b-bg-second: #111;
--b-accent: #6664AF;
--b-accent-text: #fff;
--b-text: #fff;
--b-border: #fff;
--b-button-bg: #6664AF;
--b-red: #800;
--b-green: #080;
--b-blue: #000066;
--b-orange: #f80;
}

5
src/routes/+layout.js Normal file
View File

@ -0,0 +1,5 @@
// Tauri doesn't have a Node.js server to do proper SSR
// so we will use adapter-static to prerender the app (SSG)
// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info
export const prerender = true;
export const ssr = false;

776
src/routes/+page.svelte Normal file
View File

@ -0,0 +1,776 @@
<script>
import "$lib/global.css";
import { path } from "@tauri-apps/api";
import Database from "@tauri-apps/plugin-sql";
// import { writeTextFile, BaseDirectory } from "@tauri-apps/plugin-fs";
import ScreenWrap from "$lib/components/ui/ScreenWrap.svelte";
import Dialog from "$lib/components/ui/Dialog.svelte";
import Field from "$lib/components/form/Field.svelte";
import TextInput from "$lib/components/form/TextInput.svelte";
import NumberInput from "$lib/components/form/NumberInput.svelte";
// import DateInput from "$lib/components/form/DateInput.svelte";
import PhoneInput from "$lib/components/form/PhoneInput.svelte";
import Select from "$lib/components/form/Select.svelte";
import Table from "$lib/components/table/Table.svelte";
import Tr from "$lib/components/table/Tr.svelte";
import Td from "$lib/components/table/Td.svelte";
import Atd from "$lib/components/table/Atd.svelte";
import TableCudButtons from "$lib/components/shorts/TableCUDButtons.svelte";
import TableButtonsWrap from "$lib/components/shorts/TableButtonsWrap.svelte";
import ViewTable from "$lib/components/shorts/ViewTable.svelte";
import TableListShort from "$lib/components/shorts/TableListShort.svelte";
/** @type {Database} */
let db;
async function load_db() {
// dev
db = await Database.load(`sqlite:${await path.resolve()}/../db/data.db`);
// db = await Database.load(`sqlite:${await path.resolve()}/db/data.db`);
await db.execute(`
CREATE TABLE IF NOT EXISTS EmployeeStatus (id INTEGER PRIMARY KEY, name VARCHAR(256));
CREATE TABLE IF NOT EXISTS Employees (
id INTEGER PRIMARY KEY,
employee_status_id INTEGER REFERENCES EmployeeStatus(id) ON DELETE CASCADE,
fname VARCHAR(100),
sname VARCHAR(100),
tname VARCHAR(100)
);
CREATE TABLE IF NOT EXISTS AccountAccessLevel (id INTEGER PRIMARY KEY, name VARCHAR(256));
CREATE TABLE IF NOT EXISTS Accounts (
id INTEGER PRIMARY KEY,
employee_id INTEGER REFERENCES Employees(id),
account_access_level_id INTEGER REFERENCES AccountAccessLevel(id),
login VARCHAR(256),
password VARCHAR(256)
);
CREATE TABLE IF NOT EXISTS Clients (
id INTEGER PRIMARY KEY,
phone VARCHAR(36),
fname VARCHAR(100),
sname VARCHAR(100),
tname VARCHAR(100)
);
CREATE TABLE IF NOT EXISTS RequestStatus (id INTEGER PRIMARY KEY, name VARCHAR(256));
CREATE TABLE IF NOT EXISTS RequestType (id INTEGER PRIMARY KEY, name VARCHAR(256));
CREATE TABLE IF NOT EXISTS Requests (
id INTEGER PRIMARY KEY,
client_id INTEGER REFERENCES Clients(id) ON DELETE CASCADE,
employee_id INTEGER REFERENCES Employees(id) ON DELETE CASCADE,
request_status_id INTEGER REFERENCES RequestStatus(id) ON DELETE CASCADE DEFAULT 1,
request_type_id INTEGER REFERENCES RequestType(id) ON DELETE CASCADE,
initial_date DATE DEFAULT CURRENT_TIMESTAMP, -- not editable by user
description TEXT,
duration FLOAT NULL -- hours
);
`);
}
/** @typedef Page
* @property {Object} tables
* @property {Object} viewes
*/
const db_scheme = {
/** @type Page */
Запросы: {
/** @type {any} */
tables: {
Requests: "Запросы",
Clients: "Клиенты",
},
/** @type {any} */
viewes: {
RequestCount: "Количество заявок",
AvgRequestTime: "Среднее время заявок",
StatByType: "Статистика по типам",
},
},
/** @type Page */
Сотрудники: {
/** @type {any} */
tables: {
Employees: "Сотрудники",
Accounts: "Аккаунты",
AccountAccessLevel: "Доступ",
},
/** @type {any} */
viewes: {},
},
/** @type Page */
Списки: {
/** @type {any} */
tables: {
RequestStatus: "Статус запроса",
RequestType: "Тип запроса",
EmployeeStatus: "Должность",
},
/** @type {any} */
viewes: {},
},
};
const data_access = {
admin: true,
dicrector: {
Запросы: [
"Requests",
"Clients",
"RequestCount",
"AvgRequestTime",
"StatByType",
],
Сотрудники: ["Employees", "Accounts"],
Списки: ["RequestStatus", "RequestType", "EmployeeStatus"],
},
worker: {
Запросы: ["Requests", "Clients"],
},
};
/** @type {any} */
export let current_item = {};
let is_item_dialog_open = false;
let is_dialog_item_add = false;
/**
* @param {string} access_level
* @param {string} tab_name
* @param {string | null} name
*/
function check_access(access_level, tab_name, name) {
// @ts-ignore
if (data_access[access_level] === true) return true;
// @ts-ignore
if (Object.hasOwn(data_access[access_level], tab_name) && name === null)
return true;
// @ts-ignore
return data_access[access_level][tab_name].includes(name);
}
/**
@param {string} login
@param {string} passowrd
*/
async function check_login(login, passowrd) {
const access_type =
await db.select(`SELECT AccountAccessLevel.name FROM Accounts
JOIN AccountAccessLevel ON AccountAccessLevel.id = Accounts.account_access_level_id
WHERE Accounts.login = "${login}" AND Accounts.password = "${passowrd}";`);
if (access_type.length === 0) {
return [false, ""];
} else {
return [true, access_type[0].name];
}
}
</script>
<ScreenWrap
{db}
{db_scheme}
{data_access}
{load_db}
{check_access}
{check_login}
bind:is_item_dialog_open
bind:is_dialog_item_add
bind:current_item
let:current_page
let:current_table
let:current_view
let:is_view_open
let:cur_dialog_name
let:toggle_add_dialog
let:open_item_edit
let:sql_update_short
let:sql_insert_short
let:sql_delete_short
>
{#if !is_view_open && current_page === "Запросы"}
{#if current_table === "Clients"}
<Dialog
bind:is_open={is_item_dialog_open}
name={cur_dialog_name}
let:form
>
{#if !is_dialog_item_add}
<h1>id {current_item.id}</h1>
{/if}
<Field name={"сотовый"}>
<PhoneInput bind:phone={current_item.phone}></PhoneInput>
</Field>
<Field name={"Имя"}>
<TextInput bind:value={current_item.fname}></TextInput>
</Field>
<Field name={"Фамилия"}>
<TextInput bind:value={current_item.sname}></TextInput>
</Field>
<Field name={"Отчество"}>
<TextInput bind:value={current_item.tname}></TextInput>
</Field>
{@const cur_cols = ["phone", "fname", "sname", "tname"]}
<TableCudButtons
{form}
bind:is_dialog_item_add
on_add={async () => {
await sql_insert_short(current_table, cur_cols);
}}
on_save={async () => {
await sql_update_short(current_table, cur_cols);
}}
on_delete={sql_delete_short}
></TableCudButtons>
</Dialog>
<TableButtonsWrap
{current_table}
{current_item}
{toggle_add_dialog}
on_add_click={() => {
current_item = {
id: 0,
phone: "+7 (",
fname: "",
sname: "",
tname: "",
};
}}
columns={["phone", "fname", "sname", "tname"]}
let:is_searching
let:search_query_result
>
<Table
{is_searching}
search_query={db.select(search_query_result)}
columns={["id", "сотовый", "фио"]}
query={db.select(`select * from ${current_table}`)}
let:item
let:index
>
<Tr {index} on_click={() => open_item_edit(item)}>
<Atd {item} names={["id", "phone"]}></Atd>
<Td>{item.sname} {item.fname[0]}.{item.tname[0]}</Td>
</Tr>
</Table>
</TableButtonsWrap>
{:else if current_table === "Requests"}
<Dialog
bind:is_open={is_item_dialog_open}
name={cur_dialog_name}
let:form
>
{#if !is_dialog_item_add}
<h1>id {current_item.id}</h1>
{/if}
<Field name={"Клиент"}>
<Select
bind:value={current_item.client_id}
query={db.select("select id, fname, sname, tname from Clients")}
let:vtype
>
{vtype.id}
{vtype.sname}
{vtype.sname[0]}. {vtype.tname[0]}.
</Select>
</Field>
<Field name={"Ответсвтенный"}>
<Select
bind:value={current_item.employee_id}
query={db.select("select id, fname, sname, tname from Employees")}
let:vtype
>
{vtype.id}
{vtype.sname}
{vtype.sname[0]}. {vtype.tname[0]}.
</Select>
</Field>
{#if !is_dialog_item_add}
<Field name={"Статус"}>
<Select
bind:value={current_item.request_status_id}
query={db.select("select id, name from RequestStatus")}
let:vtype
>
{vtype.id}
{vtype.name}
</Select>
</Field>
{/if}
<Field name={"Тип запроса"}>
<Select
bind:value={current_item.request_type_id}
query={db.select("select id, name from RequestType")}
let:vtype
>
{vtype.id}
{vtype.name}
</Select>
</Field>
{#if !is_dialog_item_add}
<h1>Дата начала {current_item.initial_date}</h1>
{/if}
<Field name={"Описание запроса"}>
<TextInput is_input={false} bind:value={current_item.description}
></TextInput>
</Field>
{#if !is_dialog_item_add}
<Field name={"Время"}>
<NumberInput
is_required={false}
is_float={true}
bind:value={current_item.duration}
></NumberInput>
</Field>
{/if}
{@const cur_cols = [
"client_id",
"employee_id",
"request_status_id",
"request_type_id",
"initial_date",
"description",
"duration",
]}
<TableCudButtons
{form}
bind:is_dialog_item_add
on_add={async () => {
await sql_insert_short(current_table, [
"client_id",
"employee_id",
"request_status_id",
"request_type_id",
"description",
"duration",
]);
}}
on_save={async () => {
await sql_update_short(current_table, cur_cols);
}}
on_delete={sql_delete_short}
></TableCudButtons>
</Dialog>
<TableButtonsWrap
{current_table}
{current_item}
{toggle_add_dialog}
on_add_click={() => {
current_item = {
id: 0,
client_id: 0,
employee_id: 1,
request_status_id: 1,
request_type_id: 1,
description: "",
duration: null,
};
}}
columns={[
"client_id",
"employee_id",
"request_status_id",
"request_type_id",
"initial_date",
"description",
"duration",
]}
ex_params={[
{
name: "Clients.fname",
is_number: false,
},
{
name: "Clients.sname",
is_number: false,
},
{
name: "Clients.tname",
is_number: false,
},
{
name: "Employees.fname",
is_number: false,
},
{
name: "Employees.sname",
is_number: false,
},
{
name: "Employees.tname",
is_number: false,
},
{
name: "RequestStatus.name",
is_number: false,
},
{
name: "RequestType.name",
is_number: false,
},
]}
joins={`
JOIN Clients on Clients.id = Requests.client_id
JOIN Employees on Employees.id = Requests.employee_id
JOIN RequestStatus on RequestStatus.id = Requests.request_status_id
JOIN RequestType on RequestType.id = Requests.request_type_id
`}
let:is_searching
let:search_query_result
>
<Table
{is_searching}
search_query={db.select(search_query_result)}
columns={[
"id",
"клиент",
"ответсвенный",
"статус",
"тип",
"дата начала",
// "описание",
"длительность",
]}
query={db.select(`select * from ${current_table}`)}
let:item
let:index
>
<Tr {index} on_click={() => open_item_edit(item)}>
<Td>
{item.id}
</Td>
<Td>
{#await db.select(`select fname, sname, tname from Clients where id = ${item.client_id}`) then data}
{data[0].fname} {data[0].sname[0]}. {data[0].tname[0]}.
{/await}
</Td>
<Td>
{#await db.select(`select id, fname, sname, tname from Employees where id = ${item.employee_id}`) then data}
{data[0].fname} {data[0].sname[0]}. {data[0].tname[0]}.
{/await}
</Td>
<Td>
{#await db.select(`select id, name from RequestStatus where id = ${item.request_status_id}`) then data}
{data[0].name}
{/await}
</Td>
<Td>
{#await db.select(`select id, name from RequestType where id = ${item.request_type_id}`) then data}
{data[0].name}
{/await}
</Td>
<Atd {item} names={["initial_date", "duration"]}></Atd>
</Tr>
</Table>
</TableButtonsWrap>
{/if}
{:else if is_view_open}
{#if current_view === "RequestCount"}
<ViewTable
query={db.select(
`SELECT 'значение' AS name, COUNT(id) AS value FROM Requests;`,
)}
columns={["name", "aa"]}
names={["name", "value"]}
></ViewTable>
{:else if current_view === "AvgRequestTime"}
<ViewTable
query={db.select(
`SELECT 'значение' AS name, AVG(duration) AS value
FROM Requests
WHERE duration NOT NULL;`,
)}
columns={["name", "value"]}
names={["name", "value"]}
></ViewTable>
{:else if current_view === "StatByType"}
<ViewTable
query={db.select(`
SELECT
RequestType.name AS type_name,
AVG(duration) AS avg_duratino,
COUNT(request_type_id) as type_count
FROM Requests
JOIN RequestType on RequestType.id = Requests.request_type_id
WHERE duration
GROUP BY request_type_id;
`)}
columns={["название", "среднее время", "количество"]}
names={["type_name", "avg_duratino", "type_count"]}
></ViewTable>
{/if}
{/if}
{#if current_page === "Сотрудники"}
{#if current_table === "Employees"}
<Dialog
bind:is_open={is_item_dialog_open}
name={cur_dialog_name}
let:form
>
{#if !is_dialog_item_add}
<h1>id {current_item.id}</h1>
{/if}
<Field name={"Должность"}>
<Select
bind:value={current_item.employee_status_id}
query={db.select("select id, name from EmployeeStatus")}
let:vtype
>
{vtype.id}
{vtype.name}
</Select>
</Field>
<Field name={"Имя"}>
<TextInput bind:value={current_item.fname}></TextInput>
</Field>
<Field name={"Фамилия"}>
<TextInput bind:value={current_item.sname}></TextInput>
</Field>
<Field name={"Отчество"}>
<TextInput bind:value={current_item.tname}></TextInput>
</Field>
{@const cur_cols = ["employee_status_id", "fname", "sname", "tname"]}
<TableCudButtons
{form}
bind:is_dialog_item_add
on_add={async () => {
await sql_insert_short(current_table, cur_cols);
}}
on_save={async () => {
await sql_update_short(current_table, cur_cols);
}}
on_delete={sql_delete_short}
></TableCudButtons>
</Dialog>
<TableButtonsWrap
{current_table}
{current_item}
{toggle_add_dialog}
on_add_click={() => {
current_item = {
id: 0,
employee_status_id: 1,
fname: "",
sname: "",
tname: "",
};
}}
columns={["employee_status_id", "fname", "sname", "tname"]}
joins={`JOIN EmployeeStatus on EmployeeStatus.id = Employees.employee_status_id`}
ex_params={[
{
name: "EmployeeStatus.name",
is_number: false,
},
]}
let:is_searching
let:search_query_result
>
<Table
{is_searching}
search_query={db.select(search_query_result)}
columns={["id", "должность", "фио"]}
query={db.select(`select * from ${current_table}`)}
let:item
let:index
>
<Tr {index} on_click={() => open_item_edit(item)}>
<Atd {item} names={["id"]}></Atd>
<Td>
{#await db.select(`select name from EmployeeStatus where id = ${item.employee_status_id}`) then data}
{data[0].name}
{/await}
</Td>
<Td>{item.sname} {item.fname[0]}.{item.tname[0]}</Td>
</Tr>
</Table>
</TableButtonsWrap>
{:else if current_table === "Accounts"}
<Dialog
bind:is_open={is_item_dialog_open}
name={cur_dialog_name}
let:form
>
{#if !is_dialog_item_add}
<h1>id {current_item.id}</h1>
{/if}
<Field name={"Сотрудник"}>
<Select
bind:value={current_item.employee_id}
query={db.select("select id, fname, sname, tname from Employees")}
let:vtype
>
{vtype.id}
{vtype.fname}
{vtype.sname[0]}. {vtype.tname[0]}.
</Select>
</Field>
<Field name={"Доступ"}>
<Select
bind:value={current_item.account_access_level_id}
query={db.select("select id, name from AccountAccessLevel")}
let:vtype
>
{vtype.id}
{vtype.name}
</Select>
</Field>
<Field name={"login"}>
<TextInput bind:value={current_item.login}></TextInput>
</Field>
<Field name={"Пароль"}>
<TextInput bind:value={current_item.password}></TextInput>
</Field>
{@const cur_cols = [
"employee_id",
"account_access_level_id",
"login",
"password",
]}
<TableCudButtons
{form}
bind:is_dialog_item_add
on_add={async () => {
await sql_insert_short(current_table, cur_cols);
}}
on_save={async () => {
await sql_update_short(current_table, cur_cols);
}}
on_delete={sql_delete_short}
></TableCudButtons>
</Dialog>
<TableButtonsWrap
{current_table}
{current_item}
{toggle_add_dialog}
on_add_click={() => {
current_item = {
id: 0,
employee_id: 0,
account_access_level_id: 0,
login: "",
password: "",
};
}}
columns={[
"employee_id",
"account_access_level_id",
"login",
"password",
]}
joins={"JOIN Employees on Accounts.employee_id = Employees.id"}
ex_params={[
{
name: "Employees.fname",
is_number: false,
},
{
name: "Employees.sname",
is_number: false,
},
{
name: "Employees.tname",
is_number: false,
},
]}
let:is_searching
let:search_query_result
>
<Table
{is_searching}
search_query={db.select(search_query_result)}
columns={["id", "Сотрудник", "Доступ", "Логин"]}
query={db.select(`select * from ${current_table}`)}
let:item
let:index
>
<Tr {index} on_click={() => open_item_edit(item)}>
<Atd {item} names={["id"]}></Atd>
<Td>
{#await db.select(`select id, fname, sname, tname from Employees where id = ${item.employee_id}`) then data}
{data[0].fname} {data[0].sname[0]}. {data[0].tname[0]}.
{/await}
</Td>
<Td>
{#await db.select(`select name from AccountAccessLevel where id = ${item.account_access_level_id}`) then data}
{data[0].name}
{/await}
</Td>
<Atd {item} names={["login"]}></Atd>
</Tr>
</Table>
</TableButtonsWrap>
{:else if current_table === "AccountAccessLevel"}
<TableListShort
{db}
{cur_dialog_name}
{current_table}
bind:is_item_dialog_open
bind:is_dialog_item_add
bind:current_item
query={db.select(`select * from ${current_table}`)}
columns={["id", "name"]}
columns_display={["id", "название"]}
{toggle_add_dialog}
{open_item_edit}
{sql_insert_short}
{sql_update_short}
{sql_delete_short}
></TableListShort>
{/if}
{/if}
{#if !is_view_open && current_page === "Списки"}
{#if ["RequestStatus", "RequestType", "EmployeeStatus"].includes(current_table)}
<TableListShort
{db}
{cur_dialog_name}
{current_table}
bind:is_item_dialog_open
bind:is_dialog_item_add
bind:current_item
query={db.select(`select * from ${current_table}`)}
columns={["id", "name"]}
columns_display={["id", "название"]}
{toggle_add_dialog}
{open_item_edit}
{sql_insert_short}
{sql_update_short}
{sql_delete_short}
></TableListShort>
{/if}
{/if}
</ScreenWrap>

BIN
static/jbmv.ttf Normal file

Binary file not shown.

13
svelte.config.js Normal file
View File

@ -0,0 +1,13 @@
import adapter from "@sveltejs/adapter-static";
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter(),
},
preprocess: vitePreprocess()
};
export default config;

10
tailwind.config.js Normal file
View File

@ -0,0 +1,10 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
darkMode: 'selector',
theme: {
extend: {},
},
plugins: [],
}

32
vite.config.js Normal file
View File

@ -0,0 +1,32 @@
import { defineConfig } from "vite";
import { sveltekit } from "@sveltejs/kit/vite";
// @ts-expect-error process is a nodejs global
const host = process.env.TAURI_DEV_HOST;
// https://vitejs.dev/config/
export default defineConfig(async () => ({
plugins: [sveltekit()],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
//
// 1. prevent vite from obscuring rust errors
clearScreen: false,
// 2. tauri expects a fixed port, fail if that port is not available
server: {
port: 1420,
strictPort: true,
host: host || false,
hmr: host
? {
protocol: "ws",
host,
port: 1421,
}
: undefined,
watch: {
// 3. tell vite to ignore watching `src-tauri`
ignored: ["**/src-tauri/**"],
},
},
}));