From 1cb0bc779350fc9a405562857442cebbe4011acf Mon Sep 17 00:00:00 2001 From: awenn2015 Date: Wed, 31 May 2023 18:43:02 +0400 Subject: [PATCH] update to v2.1.5-beta-3 --- package.json | 2 +- src/defines.ts | 4 +- src/hooks/useLog.ts | 4 +- src/service/ImportService.ts | 122 ++++++++++++++++++ src/service/TableService.ts | 4 +- src/temps/App.tsx | 59 +++++---- .../{table/TableButtons.tsx => Bottom.tsx} | 119 ++++++----------- src/temps/Container.tsx | 26 ++-- src/temps/Updates.tsx | 44 +++++-- src/temps/modals/HelpModal.tsx | 3 +- src/temps/setting/EntityRepeater.tsx | 4 +- src/temps/table/TableHead.tsx | 70 +++++----- src/types/index.d.ts | 4 +- src/utils/index.ts | 8 +- 14 files changed, 304 insertions(+), 169 deletions(-) create mode 100644 src/service/ImportService.ts rename src/temps/{table/TableButtons.tsx => Bottom.tsx} (64%) diff --git a/package.json b/package.json index 1309429..8914111 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "working-hours", "homepage": "/working-hours/", - "version": "2.1.5-beta-1", + "version": "2.1.5-beta-3", "private": true, "dependencies": { "bootstrap": "^5.2.3", diff --git a/src/defines.ts b/src/defines.ts index 792c8e4..dc7da19 100644 --- a/src/defines.ts +++ b/src/defines.ts @@ -4,6 +4,6 @@ // prev = 214023 export const appVersion = { - name: '2.1.5-beta-1', - code: 215021, + name: '2.1.5-beta-3', + code: 215023, } \ No newline at end of file diff --git a/src/hooks/useLog.ts b/src/hooks/useLog.ts index 9e32414..f3f562f 100644 --- a/src/hooks/useLog.ts +++ b/src/hooks/useLog.ts @@ -1,7 +1,7 @@ -import { useEffect } from "react" +import { useEffect } from 'react' function useLog(data: any, deps: any[] = []) { - deps = deps.length ? deps : [data] + deps = deps.length ? [data, ...deps] : [data] useEffect(() => { console.log(data) diff --git a/src/service/ImportService.ts b/src/service/ImportService.ts new file mode 100644 index 0000000..e8af053 --- /dev/null +++ b/src/service/ImportService.ts @@ -0,0 +1,122 @@ +import { ITableOptionsEntity, IWorkTableRow } from '@/types' + +type EventHandlers = { + update: null | ((list: ITableOptionsEntity[]) => void), + success: null | ((table: IWorkTableRow[]) => void) +} + +class ImportService { + private reader: FileReader + private handlers: EventHandlers + + public constructor( + file: File | Blob, + private entities: ITableOptionsEntity[], + private active: string, + ) { + this.reader = new FileReader() + this.reader.readAsText(file) + + this.reader.onload = this.onload.bind(this) + + this.reader.onerror = () => { + this.onerror(this.reader.error) + } + + this.handlers = { + update: null, + success: null, + } + } + + /* ========================================== */ + + public onUpdateEntity(fn: (data: ITableOptionsEntity[]) => void) { + this.handlers.update = fn + } + + public onSuccess(fn: (table: IWorkTableRow[]) => void) { + this.handlers.success = fn + } + + /* ========================================== */ + + private insertEntities(entity: string[], prevTech: string[]) { + if (!this.handlers.update) return + + const list = entity.filter(key => !prevTech.includes(key)).map((it, i) => ({ + key: it, text: `Текст - ${i + 1}`, rate: 100, + })) + + this.handlers.update(list) + } + + private onload() { + if (!this.reader.result) return + if (!this.handlers.success) return + + try { + const any = JSON.parse(this.reader.result as string) + const data = this.validate(any) + if (!data.length) return + + const entity = data.reduce((list, it) => { + if (!list.includes(it.entity)) list.push(it.entity) + return list + }, []) + + const prevTech = this.entities.map(({ key }) => key) + const extra = [...entity, ...prevTech] + + if (extra.length !== prevTech.length) + this.insertEntities(entity, prevTech) + + this.handlers.success(data) + } catch (err) { + this.onerror(err) + } + } + + private onerror(err: any) { + console.error(err) + alert('Не удалось импортировать файл!') + } + + private validate(data: any): IWorkTableRow[] { + if (!Array.isArray(data)) { + alert('Ошибка импорта, не валидный формат данных!') + return [] + } + + const schema: { key: string, type: string, empty?: boolean }[] = [ + { key: 'id', type: 'string', empty: false }, + { key: 'start', type: 'string', empty: false }, + { key: 'finish', type: 'string', empty: false }, + { key: 'entity', type: 'string', empty: false }, + { key: 'isPaid', type: 'boolean' }, + { key: 'description', type: 'string', empty: true }, + ] + + const list = [] + + for (const it of data) { + if (!(typeof it === 'object' && !Array.isArray(it))) + return [] + + const check = schema.every(({ key, type, empty }) => { + return key in it && typeof it[key] === type && + (type === 'string' && !empty ? it[key].trim() !== '' : true) + }) + + if (!check) continue + else { + it['tableId'] = this.active + list.push(it) + } + } + + return list + } +} + +export default ImportService \ No newline at end of file diff --git a/src/service/TableService.ts b/src/service/TableService.ts index cf5002a..123bb24 100644 --- a/src/service/TableService.ts +++ b/src/service/TableService.ts @@ -89,12 +89,12 @@ class TableService { public static deleteWorkTable(id: string): IWorkTable[] | false { localStorage.removeItem(getLsTableKey(id)) + localStorage.removeItem(getLsOptionsKey(id)) const list = this.listOfTablesInfo const index = list.findIndex(it => it.id === id) - if (index === -1) - return false + if (index === -1) return false list.splice(index, 1) this.listOfTablesInfo = list diff --git a/src/temps/App.tsx b/src/temps/App.tsx index 5e1b46c..079df30 100644 --- a/src/temps/App.tsx +++ b/src/temps/App.tsx @@ -11,9 +11,9 @@ import { wrapPayload, } from 'context/TableContext' import { useEffect, useReducer, useState } from 'react' -import TableButtons from './table/TableButtons' +import Bottom from './Bottom' import Filter from './filter/Filter' -import { getAllIds, getTypedKeys } from '@/utils' +import { getAllIds, getTypedKeys, localStorageKeys } from '@/utils' import Left from './left/Left' import DescrModal from './modals/DescrModal' import TableService from '@/service/TableService' @@ -21,10 +21,10 @@ import useDidUpdateEffect from '@/hooks/useDidUpdateEffect' import Empty from './empty/Empty' import SettingModal from './setting/SettingModal' import CompareData from '@/utils/class/CompareData' -import { appVersion } from '@/defines' import { getAppSettings } from '@/utils/login' import HelpModal from '@/temps/modals/HelpModal' import AddingModal from '@/temps/modals/AddingModal' +import { getLsOptionsKey } from '@/data' type BoundPartsOfStore = Pick @@ -46,27 +46,40 @@ function getBoundPartsOfStore(table: IWorkTableRow[]): BoundPartsOfStore { } } -function getActiveOptions(id: string | null, def = defOptions) { - if (appVersion.code < 213023) { - console.warn('Need reformat legacy options!') - return def - } +function qtyObjKeys(obj: Object): number { + return Object.keys(obj).length +} + +function validateOptions(opt: ITableOptions, def: ITableOptions) { + const verify = ['hiddenCols', 'usingKeys'] + + return getTypedKeys(def).reduce>((list, key) => { + if (!(key in opt)) + list[key] = def[key] + else if (verify.includes(key)) + list[key] = qtyObjKeys(opt[key]) === qtyObjKeys(def[key]) + ? opt[key] : def[key] + else if (key === 'listOfTech') + list[key] = opt[key].length ? opt[key] : def[key] + else + list[key] = opt[key] - let check = false + return list + }, {}) as ITableOptions +} + +function getActiveOptions(id: string | null, def = defOptions) { + let validate = false const options = id ? (() => { - check = true + validate = true return TableService.getActiveOptions(id) })() ?? def : def - if (check) { - return getTypedKeys(def).reduce>((list, key) => { - list[key] = key in options ? options[key] : def[key] - return list - }, {}) as ITableOptions - } - - return options + if (validate) + return validateOptions(options, def) + else + return options } function getInitStore(): ITableStore { @@ -131,14 +144,14 @@ function App() { }, [store.activeTable]) return width >= 768 ? ( - - + + {store.activeTable === null ? : (<> - + @@ -149,8 +162,8 @@ function App() { )} - - + + ) : (
diff --git a/src/temps/table/TableButtons.tsx b/src/temps/Bottom.tsx similarity index 64% rename from src/temps/table/TableButtons.tsx rename to src/temps/Bottom.tsx index 00e6e87..75170a4 100644 --- a/src/temps/table/TableButtons.tsx +++ b/src/temps/Bottom.tsx @@ -1,12 +1,13 @@ import React from 'react' -import CompareData from 'utils/class/CompareData' -import { ITableOptions, IWorkTableRow } from 'types' -import { getAllIds, getDateTimeWithOffset, getFormattedDateTime, roundDateTime } from 'utils' -import Random from 'utils/class/Random' -import { Actions, useTableContext } from 'context/TableContext' +import CompareData from '@/utils/class/CompareData' +import { IWorkTableRow } from '@/types' +import { getAllIds, getDateTimeWithOffset, getFormattedDateTime, roundDateTime } from '@/utils' +import Random from '@/utils/class/Random' +import { Actions, useTableContext } from '@/context/TableContext' import TableService from '@/service/TableService' +import ImportService from '@/service/ImportService' -const TableButtons = () => { +const Bottom = () => { const [{ initialTable, modifiedTable, @@ -90,14 +91,7 @@ const TableButtons = () => { } function showExportData() { - const list = TableService.getActiveTableData(activeTable!) - - if (!list.length) { - alert('База рабочих часов пуста!') - return - } - - window.navigator.clipboard.writeText(JSON.stringify(list)) + window.navigator.clipboard.writeText(JSON.stringify(modifiedTable)) .then(() => { alert('База рабочих часов успешно скопирована в буфер обмена') }) @@ -107,71 +101,10 @@ const TableButtons = () => { }) } + // TODO: Вынести в модалку импорта function importTableData() { let overwrite = false - function handler(e: any) { - const file = e?.target?.files?.[0] - if (!file) return - - const reader = new FileReader() - reader.readAsText(file) - - // TODO: Добавить валидацию таблицы - reader.onload = function () { - if (!reader.result) return - - console.log(overwrite) - - try { - const data = JSON.parse(reader.result as string) as IWorkTableRow[] - const entity = data.reduce((list, it) => { - if (!list.includes(it.entity)) list.push(it.entity) - return list - }, []) - - const prevTech = options.listOfTech.map(({ key }) => key) - const extra = [...entity, ...prevTech] - - if (extra.length !== prevTech.length) { - const list = entity.filter(key => !prevTech.includes(key)).map((it, i) => ({ - key: it, text: `Текст - ${i + 1}`, rate: 100, - })) - - const update: ITableOptions = { - ...options, listOfTech: overwrite - ? list : [...options.listOfTech, ...list], - } - - dispatch({ - type: Actions.Rewrite, - payload: payload('options', update), - }) - - TableService.updateActiveOptions(activeTable!, update) - } - - const tables = overwrite ? data : [...modifiedTable, ...data] - - dispatch({ - type: Actions.State, - payload: { - modifiedTable: tables, - selectedRows: tables.map(({ id }) => id), - }, - }) - } catch (err) { - console.error(err) - alert('Не удалось прочитать файл!') - } - } - - reader.onerror = function () { - console.error(reader.error) - alert('Не удалось прочитать файл!') - } - } - function refine() { const actions = [['no', '0', 'n', 'нет'], ['yes', '1', 'y', 'да']] const msg = `Таблица не пуста, выберите действие: перезаписать или объединить (y/n)` @@ -189,6 +122,35 @@ const TableButtons = () => { return true } + function handler(e: any) { + if (!e.target) return + const file = e.target.files?.[0] + if (!file) return + + const service = new ImportService(file, options.listOfTech, activeTable!) + + service.onUpdateEntity((entities) => { + TableService.updateActiveOptions(activeTable!, { + ...options, listOfTech: overwrite + ? entities : [...options.listOfTech, ...entities], + }) + }) + + service.onSuccess((table) => { + table = overwrite ? table : [...modifiedTable, ...table] + TableService.updateActiveTableData(activeTable!, table) + const list = TableService.listOfTablesInfo + + for (let it of list) { + if (it.id !== activeTable) continue + it.count = table.length + } + + TableService.listOfTablesInfo = list + window.location.reload() + }) + } + if (initialTable.length > 0 && !refine()) return @@ -197,8 +159,8 @@ const TableButtons = () => { input.setAttribute('type', 'file') input.setAttribute('accept', '.json') input.addEventListener('change', handler) + input.click() - input.removeEventListener('change', handler) } return ( @@ -211,6 +173,7 @@ const TableButtons = () => { value="Экспорт" className="btn btn-outline-dark" onClick={showExportData} + disabled={!modifiedTable.length} /> { ) } -export default TableButtons \ No newline at end of file +export default Bottom \ No newline at end of file diff --git a/src/temps/Container.tsx b/src/temps/Container.tsx index c4d973c..1dab1a5 100644 --- a/src/temps/Container.tsx +++ b/src/temps/Container.tsx @@ -1,17 +1,27 @@ -import { FC, ReactNode } from 'react' +import { FC, ReactNode, useMemo } from 'react' +import { useTableContext } from '@/context/TableContext' +import { getQtyCols } from '@/utils' type WrapperProps = { children: ReactNode } -const Container: FC = ({ children }) => ( -
-
-
- {children} +const Container: FC = ({ children }) => { + const [{ options: { hiddenCols } }] = useTableContext() + + const qty = useMemo(() => { + return getQtyCols(hiddenCols) + }, [hiddenCols]) + + return ( +
+
+
+ {children} +
-
-) + ) +} export default Container \ No newline at end of file diff --git a/src/temps/Updates.tsx b/src/temps/Updates.tsx index 47a6f94..52c67d0 100644 --- a/src/temps/Updates.tsx +++ b/src/temps/Updates.tsx @@ -4,8 +4,9 @@ import UpdateModal from '@/temps/modals/UpdateModal' import { getLocalVersion, LS_VERSION_KEY } from '@/data' import TableService from '@/service/TableService' import { appVersion } from '@/defines' +import { localStorageKeys } from '@/utils' -type ListOfUpdates = { when: boolean, fn: (when: boolean) => void }[] +type ListOfUpdates = { need: boolean, reformat: (need: boolean) => void }[] function reformatLegacy215021(when: boolean) { if (!when) return @@ -14,33 +15,60 @@ function reformatLegacy215021(when: boolean) { for (const { id } of list) { const table = TableService.getActiveTableData(id) + if (!table.length) return for (let i = 0; i < table.length; i++) { table[i]!.entity = table[i]?.['tech'] || table[i]!.entity delete table[i]?.['tech'] - table[i]!.tableId = id + + if (table[i]?.tableId !== id) + table[i]!.tableId = id } TableService.updateActiveTableData(id, table) } } +function reformatLegacy215023(when: boolean) { + if (!when) return + + const match = 'awenn2015_wh_options_' + const list = TableService.listOfTablesInfo + const ids = list.map(it => it.id) + + localStorageKeys((key) => { + if (!key.includes(match)) return + + const id = (() => { + const split = key.split('_') + return split[split.length - 1]! + })() + + if (ids.includes(id)) return + localStorage.removeItem(key) + }) +} + const listOfUpdates: ListOfUpdates = [ { - when: getLocalVersion() < 215021, - fn: reformatLegacy215021, + need: getLocalVersion() < 215021, + reformat: reformatLegacy215021, + }, + { + need: getLocalVersion() < 215023, + reformat: reformatLegacy215023, }, ] const Updates = () => { - const [isUpdate] = useState(listOfUpdates.some(it => it.when)) + const [isUpdate] = useState(listOfUpdates.some(it => it.need)) useEffect(() => { if (!isUpdate) return - for (const { when, fn } of listOfUpdates) { - if (!when) continue - fn(when) + for (const { need, reformat } of listOfUpdates) { + if (!need) continue + reformat(need) } localStorage.setItem(LS_VERSION_KEY, String(appVersion.code)) diff --git a/src/temps/modals/HelpModal.tsx b/src/temps/modals/HelpModal.tsx index 89558e8..3378f00 100644 --- a/src/temps/modals/HelpModal.tsx +++ b/src/temps/modals/HelpModal.tsx @@ -7,6 +7,7 @@ const list = [ 'Что бы отредактировать данные добавленной строки в таблице кликните два раза по ячейке', `Редактируемые ячейки: 'Начал', 'Закончил', 'Сущность', 'Отплачено', 'Описание'`, `Для того что бы манипулировать со строкой (переместить или удалить) используйте клавиши забитые в настройках, по умолчанию это 'Delete', 'ArrowUp' и 'ArrowDown'`, + `Формат данных json для импорта {id: string, start: string, finish: string, entity: string, isPaid: boolean, description: string}[]` ] const HelpModal = () => { @@ -33,7 +34,7 @@ const HelpModal = () => {
    {list.map((it, i) => ( -
  • {it}
  • +
  • ))}
diff --git a/src/temps/setting/EntityRepeater.tsx b/src/temps/setting/EntityRepeater.tsx index a94bd2d..3f93012 100644 --- a/src/temps/setting/EntityRepeater.tsx +++ b/src/temps/setting/EntityRepeater.tsx @@ -1,9 +1,9 @@ import React, { FC } from 'react' -import { ITableOptionsTech } from '@/types' +import { ITableOptionsEntity } from '@/types' import { RepeaterDispatch } from '@/temps/repeater/BaseRepeater' interface EntityRepeaterProps { - state: [ITableOptionsTech, RepeaterDispatch], + state: [ITableOptionsEntity, RepeaterDispatch], i: number } diff --git a/src/temps/table/TableHead.tsx b/src/temps/table/TableHead.tsx index e45e93c..8b47d13 100644 --- a/src/temps/table/TableHead.tsx +++ b/src/temps/table/TableHead.tsx @@ -1,51 +1,43 @@ -import React from 'react' -import { useTableContext } from '@/context/TableContext' -import { getTypedKeys } from '@/utils' - -const sizes = { - allShow: [5, 18, 12.5, 12.5, 10, 10, 5, 5, 22], - isNumberHide: [15, 14, 14, 10, 10, 7.5, 7.5, 22], - isEntityHide: [5, 18, 14, 14, 10, 7.5, 7.5, 24], - isDescrHide: [5, 20, 17.5, 17.5, 12.5, 12.5, 7.5, 7.5], - allHide: [20, 20, 20, 20, 10, 10], -} +import React, { useCallback } from 'react' +import { defOptions, useTableContext } from '@/context/TableContext' +import { getQtyCols, getTypedKeys } from '@/utils' +import { ITableOptionsHidden } from '@/types' +type ListOfCases = '000' | '100' | '010' | '001' | '101' | '110' | '011' | '111' type ColProps = { width?: number } -const TableHead = () => { - const [{ options: { hiddenCols } }] = useTableContext() - const initialsCols = 9 +/* ======================================== */ - function iterateCols(fn: (i: number, props: ColProps) => React.ReactNode) { - const qty = getTypedKeys(hiddenCols).reduce((num, key) => { - if (hiddenCols[key]) num -= 1 - return num - }, initialsCols) +const sizes: Record = { + '000': [5, 18, 12.5, 12.5, 10, 10, 5, 5, 22], + '100': [15, 14, 14, 10, 10, 7.5, 7.5, 22], + '010': [5, 18, 14, 14, 10, 7.5, 7.5, 24], + '001': [5, 20, 17.5, 17.5, 12.5, 12.5, 7.5, 7.5], + '101': [20, 15, 15, 15, 15, 10, 10], + '110': [20, 15, 15, 10, 7.5, 7.5, 25], + '011': [5, 25, 20, 20, 12.5, 8.75, 8.75], + '111': [20, 20, 20, 20, 10, 10], +} - const list = [] +function getCaseKey(obj: ITableOptionsHidden) { + const keys = getTypedKeys(defOptions.hiddenCols) + const arr = keys.map(key => String(Number(obj[key]))) + return arr.join('') as keyof typeof sizes +} - for (let i = 0; i < qty; i++) { - const props = ((): ColProps => { - if (qty === 9) - return { width: sizes.allShow[i]! } - else if (qty === 6) - return { width: sizes.allHide[i]! } - else if (qty === 8 && hiddenCols.number) - return { width: sizes.isNumberHide[i]! } - else if (qty === 8 && hiddenCols.entity) - return { width: sizes.isEntityHide[i]! } - else if (qty === 8 && hiddenCols.description) - return { width: sizes.isDescrHide[i]! } - else - return {} - })() +/* ======================================== */ +const TableHead = () => { + const [{ options: { hiddenCols } }] = useTableContext() - list.push(fn(i, props)) - } + const iterateCols = useCallback((fn: (i: number, props: ColProps) => React.ReactNode) => { + const key = getCaseKey(hiddenCols) + const qty = getQtyCols(hiddenCols) - return list - } + return [...Array(qty).keys()].map((i) => { + return fn(i, { width: sizes[key][i]! }) + }) + }, [hiddenCols]) return ( <> diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 12103b8..3e7ab63 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -7,7 +7,7 @@ export type IAppSettings = { isDisabled: boolean } -export type ITableOptionsTech = { key: string, text: string, rate: number } +export type ITableOptionsEntity = { key: string, text: string, rate: number } export type ListOfHiddenCol = 'number' | 'entity' | 'description' export type ITableOptionsHidden = Record @@ -17,7 +17,7 @@ export type ITableOptionsKeys = Record export type ITableOptions = { dtRoundStep: number, - listOfTech: ITableOptionsTech[] + listOfTech: ITableOptionsEntity[] hiddenCols: ITableOptionsHidden usingKeys: ITableOptionsKeys, typeOfAdding: 'fast' | 'full' diff --git a/src/utils/index.ts b/src/utils/index.ts index 9df12fb..e950795 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,4 @@ -import { IWorkTableRow } from '@/types' +import { ITableOptionsHidden, IWorkTableRow } from '@/types' import Random from './class/Random' export function getDiffOfHours(start: string, finish: string): number { @@ -153,6 +153,12 @@ export function localStorageKeys(fn: (key: string) => void) { fn(localStorage.key(i)!) } +export function getQtyCols(obj: ITableOptionsHidden, initial = 9) { + return getTypedKeys(obj).reduce((num, key) => { + return obj[key] ? num - 1 : num + }, initial) +} + // export function getTableInfoDto({ id, name, created, count }: IWorkTable): PartOfWorkTable { // return { id, name, created, count } // }