From e9c5b79c3e99c8a902d4cf48c93eb3317003f2a3 Mon Sep 17 00:00:00 2001 From: Danilo Lutz Date: Tue, 4 Jan 2022 16:10:06 -0300 Subject: [PATCH] Feat/dinamic tabs (#115) * temp: event parameters * feat: dinamic tabs * feat: saving settings * fix: a lot of things * fix: a lot of things * fix: a lot of things Co-authored-by: Andre Gava --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 2 +- README.md | 2 + package.json | 1 + src/app/components/Barcode/index.tsx | 12 + src/app/components/SearchMenu/index.tsx | 18 +- src/app/components/Settings/index.tsx | 108 ++++- src/app/components/Settings/styles.ts | 60 ++- src/app/components/Tabs/Tab.ts | 2 + src/app/components/Tabs/index.tsx | 44 +- src/app/components/Title/Create/index.tsx | 397 ++++++++++++++++ .../components/Title/{ => Create}/styles.ts | 4 + src/app/components/Title/List/index.tsx | 58 +++ src/app/components/Title/List/styles.ts | 5 + src/app/components/Title/Read/index.tsx | 134 ++++++ src/app/components/Title/Read/styles.ts | 59 +++ src/app/components/Title/Title.ts | 44 ++ src/app/components/Title/Update/index.tsx | 440 ++++++++++++++++++ src/app/components/Title/Update/styles.ts | 71 +++ src/app/components/Title/index.tsx | 403 +--------------- src/app/hooks/useBarcode.ts | 45 ++ src/app/styles/global.ts | 5 +- src/app/util/AppEventHandler.ts | 4 +- src/app/util/DefaultEntities.ts | 34 +- src/common/Actions.ts | 7 + src/electron/Main.ts | 15 + src/electron/Menu.ts | 11 +- src/electron/contracts/Repository.ts | 4 +- src/electron/database/EntityMap.ts | 2 +- src/electron/database/factory/index.ts | 6 + src/electron/database/models/Author.schema.ts | 2 +- .../database/models/Category.schema.ts | 2 +- src/electron/database/models/Title.schema.ts | 12 +- ...Author.schema.ts => TitleAuthor.schema.ts} | 2 +- .../database/models/TitleCategory.schema.ts | 2 +- .../database/repository/RepositoryBase.ts | 13 +- .../database/repository/TitleRepository.ts | 27 ++ src/locales/en-US/common.json | 5 +- src/locales/pt-BR/common.json | 5 +- yarn.lock | 5 + 40 files changed, 1628 insertions(+), 446 deletions(-) create mode 100644 src/app/components/Barcode/index.tsx create mode 100644 src/app/components/Title/Create/index.tsx rename src/app/components/Title/{ => Create}/styles.ts (97%) create mode 100644 src/app/components/Title/List/index.tsx create mode 100644 src/app/components/Title/List/styles.ts create mode 100644 src/app/components/Title/Read/index.tsx create mode 100644 src/app/components/Title/Read/styles.ts create mode 100644 src/app/components/Title/Title.ts create mode 100644 src/app/components/Title/Update/index.tsx create mode 100644 src/app/components/Title/Update/styles.ts create mode 100644 src/app/hooks/useBarcode.ts create mode 100644 src/common/Actions.ts rename src/electron/database/models/{TtitleAuthor.schema.ts => TitleAuthor.schema.ts} (87%) create mode 100644 src/electron/database/repository/TitleRepository.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4df6f3b..c3bb1b8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ jobs: strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + os: [ubuntu-latest, windows-latest, macOS-11] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 59008d4..243d608 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + os: [ubuntu-latest, windows-latest, macOS-11] steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index 2e2a2d0..6764412 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ [![Build](https://github.com/librarian-org/librarian/actions/workflows/build.yml/badge.svg)](https://github.com/librarian-org/librarian/actions/workflows/build.yml) +> **NOTE**: This project is in Work-In-Progress stage, so it's not functional yet... + A free OpenSource software to handle with books. It's ideal for school libraries and for someone who have many books. diff --git a/package.json b/package.json index d07d8ee..ddc1a51 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "i18next-electron-fs-backend": "^2.0.0", "i18next-fs-backend": "^1.1.4", "immutability-helper": "^3.1.1", + "jsbarcode": "^3.11.5", "polished": "^4.1.3", "react": "^17.0.2", "react-dnd": "^14.0.4", diff --git a/src/app/components/Barcode/index.tsx b/src/app/components/Barcode/index.tsx new file mode 100644 index 0000000..7df5794 --- /dev/null +++ b/src/app/components/Barcode/index.tsx @@ -0,0 +1,12 @@ +import React, { ReactElement } from 'react'; +import { useBarcode, BarcodeProps } from '../../hooks/useBarcode'; + +export default function Barcode(props: BarcodeProps): ReactElement { + const { value, options } = props; + const { inputRef } = useBarcode({ + value, + options, + }); + + return ; +} diff --git a/src/app/components/SearchMenu/index.tsx b/src/app/components/SearchMenu/index.tsx index edaae27..59f665e 100644 --- a/src/app/components/SearchMenu/index.tsx +++ b/src/app/components/SearchMenu/index.tsx @@ -82,6 +82,14 @@ const SearchMenu: React.FC = ({ isOpen, setOpen }) => { [searchResult, selectedItem] ); + const globalSearchHandler = useCallback((search) => { + const retorno: any = window.api.sendSync('globalSearch', { + entity: 'Any', + value: search, + }); + return retorno; + }, []); + const handleChange = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; @@ -102,17 +110,9 @@ const SearchMenu: React.FC = ({ isOpen, setOpen }) => { setMaxItems(finalResult.length * 2); setSelectedItem(-1); }, - [searchSourceMemo] + [globalSearchHandler, searchSourceMemo] ); - const globalSearchHandler = useCallback((search) => { - const retorno: any = window.api.sendSync('globalSearch', { - entity: 'Any', - value: search, - }); - return retorno; - }, []); - const handleKeys = useCallback( (event, clicked): void => { const mapKeys = [ diff --git a/src/app/components/Settings/index.tsx b/src/app/components/Settings/index.tsx index 1b8d82c..e7c0965 100644 --- a/src/app/components/Settings/index.tsx +++ b/src/app/components/Settings/index.tsx @@ -1,13 +1,111 @@ -import React from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; +import { FiSave } from 'react-icons/fi'; +import { useToast } from '../../hooks/toast'; +import i18n from '../../i18n'; +import Button from '../Button'; +import Input from '../Input'; +import { ButtonContainer, Container } from './styles'; +interface Settings { + id?: number; + daysReturnDate: string; + backupPath: string; +} +const Settings: React.FC = () => { + const { addToast } = useToast(); -import { Container } from './styles'; + const [settings, setSettings] = useState(null); + const [daysReturnDate, setDaysReturnDate] = useState('7'); + const [backupPath, setBackupPath] = useState(''); + + useEffect(() => { + const result = window.api.sendSync('list', { + entity: 'Settings', + value: {}, + }) as Settings[]; + + if (result.length > 0) { + setSettings(result[0]); + setDaysReturnDate(result[0].daysReturnDate); + setBackupPath(result[0].backupPath); + return; + } + }, []); + + const handleSave = useCallback(() => { + const result = window.api.sendSync(settings.id ? 'update' : 'create', { + entity: 'Settings', + value: settings.id + ? { + id: settings.id, + daysReturnDate: daysReturnDate, + backupPath: backupPath, + } + : { + daysReturnDate: daysReturnDate, + backupPath: backupPath, + }, + }) as { id: string }; + + addToast({ + title: i18n.t('notifications.success'), + type: 'success', + description: i18n.t('settings.saved'), + }); + + return; + }, [backupPath, daysReturnDate, settings]); -const Settings: React.FC = () => { return ( - Settings + <> +
+ setDaysReturnDate(e.target.value)} + value={daysReturnDate} + placeholder={i18n.t('settings.time')} + /> + + setBackupPath(e.target.value)} + placeholder={i18n.t('settings.path')} + /> +
+ + + + +
); }; - +declare module 'react' { + interface HTMLAttributes extends AriaAttributes, DOMAttributes { + directory?: string; + webkitdirectory?: string; + } +} export default Settings; diff --git a/src/app/components/Settings/styles.ts b/src/app/components/Settings/styles.ts index 384808a..9ac17c4 100644 --- a/src/app/components/Settings/styles.ts +++ b/src/app/components/Settings/styles.ts @@ -1,5 +1,63 @@ -import styled from 'styled-components'; +import { shade, tint } from 'polished'; +import styled, { css } from 'styled-components'; export const Container = styled.div` + display: flex; + flex-direction: column; + width: 100vw; +`; + + export const ButtonContainer = styled.div` + position: relative,; + padding: 0 24px; + display: flex; + justify-content: end; + `; + +export const List = styled.div` + display: flex; + flex-direction: column; + width: 100%; +`; + +export const ListItem = styled.div` + display: flex; + flex-direction: row; + border-radius: 8px; + + justify-content: space-between; + align-items: center; + + padding: 12px; + margin-bottom: 4px; + width: 100%; + + background-color: ${props => tint(0.2, props.theme.colors.background)}; + &:hover { + background-color: ${props => tint(0.25, props.theme.colors.background)}; + ${(props) => props.theme.title == 'light' && css` + background-color: ${(props) => shade(0.05, props.theme.colors.background)}; + `} + } + + svg { + cursor: pointer; + &:hover { + color: ${props => tint(0.2, props.theme.colors.secondary.dark)} + } + } +`; + +export const Row = styled.div` + padding: 24px 0 24px 0; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + width: 100%; +`; +export const Column = styled.div` + display: flex; `; diff --git a/src/app/components/Tabs/Tab.ts b/src/app/components/Tabs/Tab.ts index 436badf..5d24095 100644 --- a/src/app/components/Tabs/Tab.ts +++ b/src/app/components/Tabs/Tab.ts @@ -4,4 +4,6 @@ export interface Tab { title: string; unsavedChanges?: boolean; titleScope?: string; + action: string; + item: unknown; } diff --git a/src/app/components/Tabs/index.tsx b/src/app/components/Tabs/index.tsx index 3fd0fd0..1e67391 100644 --- a/src/app/components/Tabs/index.tsx +++ b/src/app/components/Tabs/index.tsx @@ -8,14 +8,19 @@ import TabContent from './TabContent'; import { Tab } from './Tab'; import Shortcuts from '../Shortcuts'; import Borrow from '../Borrow'; -import TitleCreate from '../Title'; +import Title from '../Title'; import Person from '../Person'; import Settings from '../Settings'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import update from 'immutability-helper'; import SearchMenu from '../SearchMenu'; +import { Actions } from '../../../common/Actions'; +interface ActionParameter { + action: Actions; + value?: unknown; +} interface Event { event: string; handler: (...args: any[]) => any; @@ -25,6 +30,8 @@ const Tabs: React.FC = () => { const [tabItems, setTabItems] = useState([]); const [activeTab, setActiveTab] = useState(null); const [isOpen, setOpen] = useState(false); + const [action, setAction] = useState('list'); + // const [item, setItem] = useState(undefined); const lastTab = useCallback((): Tab => { return tabItems[tabItems.length - 1]; @@ -67,12 +74,16 @@ const Tabs: React.FC = () => { }, [activeIndex, activeTab, firstTab, lastTab, setActiveTab, tabItems]); const handleCreateTab = useCallback( - (type: string) => { + (type: string, action: Actions, item: unknown) => { const hash = v4(); - const alreadyOpened = tabItems.filter((t) => t.type === type); - if (alreadyOpened.length > 0) { - setActiveTab(alreadyOpened[0]); + const tabAlreadyOpened = tabItems.filter((t) => t.type === type); + if (tabAlreadyOpened.length > 0) { + setAction(action); + tabAlreadyOpened[0].action = action; + tabAlreadyOpened[0].item = item; + setTabItems(tabItems); + setActiveTab(tabAlreadyOpened[0]); return; } @@ -80,6 +91,8 @@ const Tabs: React.FC = () => { id: hash, type: type, title: `${type}.label`, + action: action, + item: undefined }; addTab(tab); @@ -110,20 +123,23 @@ const Tabs: React.FC = () => { } }, [activeTab, close]); - const borrowTab = useCallback(() => { - handleCreateTab('borrow'); + const borrowTab = useCallback((params: CustomEvent) => { + const { action, value } = params.detail; + handleCreateTab('borrow', action, value); }, [handleCreateTab]); - const personTab = useCallback(() => { - handleCreateTab('person'); + const personTab = useCallback((params: CustomEvent) => { + const { action, value } = params.detail; + handleCreateTab('person', action, value); }, [handleCreateTab]); - const titleTab = useCallback(() => { - handleCreateTab('title'); + const titleTab = useCallback((params: CustomEvent) => { + const { action, value } = params.detail; + handleCreateTab('title', action, value); }, [handleCreateTab]); const settingsTab = useCallback(() => { - handleCreateTab('settings'); + handleCreateTab('settings', Actions.update, {}); }, [handleCreateTab]); const quickSearch = useCallback(() => { @@ -139,7 +155,7 @@ const Tabs: React.FC = () => { { event: 'settingsTab', handler: settingsTab }, { event: 'quickSearch', handler: quickSearch }, ], - [closeCurrentTab, borrowTab, personTab, titleTab, settingsTab] + [closeCurrentTab, borrowTab, personTab, titleTab, settingsTab, quickSearch] ); useEffect(() => { @@ -206,7 +222,7 @@ const Tabs: React.FC = () => { > {tab.type === 'borrow' && } {tab.type === 'person' && } - {tab.type === 'title' && } + {tab.type === 'title' && } {tab.type === 'settings' && <Settings />} </TabContent> ) diff --git a/src/app/components/Title/Create/index.tsx b/src/app/components/Title/Create/index.tsx new file mode 100644 index 0000000..e315851 --- /dev/null +++ b/src/app/components/Title/Create/index.tsx @@ -0,0 +1,397 @@ +import React, { + useCallback, + useMemo, + useRef, + useState, +} from 'react'; +import { format, parseISO } from 'date-fns'; +import { FiPlus, FiSave, FiTrash2 } from 'react-icons/fi'; +import { v4 } from 'uuid'; +import { useToast } from '../../../hooks/toast'; +import i18n from '../../../i18n'; +import AuthorSelect from '../../AuthorSelect'; +import Button from '../../Button'; +import CategorySelect from '../../CategorySelect'; +import { SelectHandles } from '../../CreatableSelectInput'; + +import Input from '../../Input'; +import PublisherSelect from '../../PublisherSelect'; +import SectionContent from '../../Sections/SectionContent'; +import SectionHeader from '../../Sections/SectionHeader'; + +import { ButtonContainer, Container, List, ListItem, Row } from './styles'; + +interface SelectType { + id: string; + name: string; +} + +interface Publisher { + publisher: SelectType; + classification: string; + edition: string; + publishedAt: Date; +} + +const TitleCreate: React.FC = () => { + const { addToast } = useToast(); + + const [title, setTitle] = useState(''); + const [isbn, setIsbn] = useState(''); + + const [selectedSection, setSelectedSection] = useState('editions'); + + const sections = useMemo(() => ['editions', 'authors', 'categories'], []); + + const [classification, setClassification] = useState(''); + const [edition, setEdition] = useState(''); + const [publishedAt, setPublishedAt] = useState(''); + const [publishers, setPublishers] = useState<Publisher[]>([]); + const refPublisher = useRef<SelectHandles>(null); + + const [authors, setAuthors] = useState<SelectType[]>([]); + const refAuthor = useRef<SelectHandles>(null); + + const [categories, setCategories] = useState<SelectType[]>([]); + const refCategory = useRef<SelectHandles>(null); + + const handleAddPublisher = useCallback(() => { + const publisher = refPublisher.current.getValue<SelectType>(); + const errors: string[] = []; + if (!publisher) { + errors.push(i18n.t('publisher.label')); + } + if (!classification) { + errors.push(i18n.t('title.classification')); + } + if (!edition) { + errors.push(i18n.t('title.edition')); + } + if (!publishedAt) { + errors.push(i18n.t('title.publishedAt')); + } + if (errors.length > 0) { + addToast({ + title: i18n.t('notifications.warning'), + type: 'error', + description: i18n + .t('title.informErrorsEdition') + .replace('#errors#', errors.join(', ')), + }); + return; + } + + if ( + publishers.filter((a) => a.publisher.name === publisher.name).length > 0 + ) { + addToast({ + title: i18n.t('notifications.warning'), + type: 'error', + description: `${publisher.name} ${i18n.t('title.hasBeenAdd')}`, + }); + return; + } + + const pub = { + classification, + edition, + publishedAt: parseISO(publishedAt), + publisher, + }; + setPublishers((oldState) => [...oldState, pub]); + + refPublisher.current.clear(); + setClassification(''); + setEdition(''); + setPublishedAt(''); + }, [addToast, classification, edition, publishedAt, publishers]); + + const handleRemovePublisher = useCallback( + (publisher: Publisher) => { + const publishersFiltered = publishers.filter((c) => c !== publisher); + setPublishers(publishersFiltered); + }, + [publishers] + ); + + const handleAddAuthor = useCallback(() => { + const author = refAuthor.current.getValue<SelectType>(); + if (!author) { + addToast({ + title: i18n.t('notifications.warning'), + type: 'error', + description: i18n.t('title.selectAuthorToAdd'), + }); + return; + } + + if (authors.filter((a) => a.name === author.name).length > 0) { + addToast({ + title: i18n.t('notifications.warning'), + type: 'error', + description: `${author.name} ${i18n.t('title.hasBeenAdd')}`, + }); + return; + } + + setAuthors((oldState) => [...oldState, author]); + refAuthor.current.clear(); + }, [addToast, authors]); + + const handleRemoveAuthor = useCallback( + (author: SelectType) => { + const authorsFiltered = authors.filter((a) => a !== author); + setAuthors(authorsFiltered); + }, + [authors] + ); + + const handleAddCategory = useCallback(() => { + const category = refCategory.current.getValue<SelectType>(); + + if (!category) { + addToast({ + title: i18n.t('notifications.warning'), + type: 'error', + description: i18n.t('title.selectCategoryToAdd'), + }); + return; + } + + if (categories.filter((a) => a.name === category.name).length > 0) { + addToast({ + title: i18n.t('notifications.warning'), + type: 'error', + description: `${category.name} ${i18n.t('title.hasBeenAdd')}`, + }); + return; + } + + setCategories((oldState) => [...oldState, category]); + refCategory.current.clear(); + }, [addToast, categories]); + + const handleRemoveCategory = useCallback( + (category: SelectType) => { + const categoriesFiltered = categories.filter((c) => c !== category); + setCategories(categoriesFiltered); + }, + [categories] + ); + + const handleSave = useCallback(() => { + const result = window.api.sendSync('create', { + entity: 'Title', + value: { + name: title, + ISBN: isbn, + }, + }) as { id: string }; + + categories.map((category) => { + window.api.sendSync('create', { + entity: 'TitleCategory', + value: { + titleId: result.id, + categoryId: category.id, + }, + }); + }); + + authors.map((author) => { + window.api.sendSync('create', { + entity: 'TitleAuthor', + value: { + titleId: result.id, + authorId: author.id, + }, + }); + }); + + publishers.map((edition) => { + window.api.sendSync('create', { + entity: 'TitlePublisher', + value: { + titleId: result.id, + publisherId: edition.publisher.id, + edition: edition.edition, + classification: edition.classification, + publishedAt: edition.publishedAt, + }, + }); + }); + }, [authors, categories, isbn, publishers, title]); + + return ( + <Container> + <> + <div + style={{ + padding: '24px', + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + }} + > + <Input + containerStyle={{ flexGrow: 1, marginRight: '18px' }} + type="text" + name="name" + label={i18n.t('title.name')} + autoFocus + onChange={(e) => setTitle(e.target.value)} + value={title} + placeholder={i18n.t('title.typeTitleName')} + /> + + <Input + type="text" + name="ISBN" + label="ISBN" + value={isbn} + onChange={(e) => setIsbn(e.target.value)} + placeholder={i18n.t('title.typeISBN')} + /> + </div> + <SectionHeader> + {sections.map((section) => ( + <a + key={section} + className={selectedSection === section ? 'active' : ''} + onClick={() => setSelectedSection(section)} + > + {i18n.t(`title.${section}`)} + </a> + ))} + </SectionHeader> + <div> + <SectionContent isActive={selectedSection === 'editions'}> + <Row style={{ minHeight: '180px' }}> + <Input + type="text" + name="classification" + label={i18n.t('title.classification')} + placeholder={i18n.t('title.typeClassification')} + value={classification} + onChange={(e) => setClassification(e.target.value)} + /> +   + <Input + type="text" + name="edition" + label={i18n.t('title.edition')} + placeholder={i18n.t('title.typeEdition')} + value={edition} + onChange={(e) => setEdition(e.target.value)} + /> +   + <Input + type="date" + name="edition_date" + label={i18n.t('title.publishedAt')} + placeholder={i18n.t('title.typePublicationDate')} + alt={i18n.t('title.typePublicationDate')} + value={publishedAt} + onChange={(e) => setPublishedAt(e.target.value)} + /> +   + <PublisherSelect + ref={refPublisher} + containerStyle={{ flexGrow: 2 }} + /> +   + <Button style={{}} color="primary" onClick={handleAddPublisher}> + <FiPlus size={20} /> + </Button> + </Row> + <Row> + <List> + {publishers.map((publisher) => ( + <ListItem key={v4()}> + <span>{publisher.classification}</span> + <span>{publisher.edition}</span> + <span> + {format(publisher.publishedAt, 'dd/MM/yyyy')} + </span> + <span>{publisher.publisher.name}</span> + <span style={{ width: '10%', textAlign: 'right' }}> + <FiTrash2 + size={20} + onClick={() => handleRemovePublisher(publisher)} + title={i18n.t('title.removeEditionInformation')} + /> + </span> + </ListItem> + ))} + </List> + </Row> + </SectionContent> + + <SectionContent isActive={selectedSection === 'authors'}> + <Row> + <AuthorSelect + ref={refAuthor} + containerStyle={{ flexGrow: 2, marginRight: '16px' }} + /> + <Button color="primary" onClick={handleAddAuthor}> + <FiPlus size={20} /> + </Button> + </Row> + <Row> + <List> + {authors.map((author) => ( + <ListItem key={v4()}> + {author.name}{' '} + <FiTrash2 + size={20} + onClick={() => handleRemoveAuthor(author)} + title={i18n.t('title.removeAuthorInformation')} + /> + </ListItem> + ))} + </List> + </Row> + </SectionContent> + + <SectionContent isActive={selectedSection === 'categories'}> + <Row> + <CategorySelect + ref={refCategory} + containerStyle={{ flexGrow: 2, marginRight: '16px' }} + /> + + <Button color="primary" onClick={handleAddCategory}> + <FiPlus size={20} /> + </Button> + </Row> + <Row> + <List> + {categories.map((category) => ( + <ListItem key={v4()}> + {category.name}{' '} + <FiTrash2 + size={20} + onClick={() => handleRemoveCategory(category)} + title={i18n.t('title.removeCategoryInformation')} + /> + </ListItem> + ))} + </List> + </Row> + </SectionContent> + </div> + <ButtonContainer> + <Button + color="primary" + title={i18n.t('button.save')} + onClick={handleSave} + > + <FiSave size={20} /> + </Button> + </ButtonContainer> + </> + </Container> + ); +}; + +export default TitleCreate; diff --git a/src/app/components/Title/styles.ts b/src/app/components/Title/Create/styles.ts similarity index 97% rename from src/app/components/Title/styles.ts rename to src/app/components/Title/Create/styles.ts index 9ac17c4..6716c6b 100644 --- a/src/app/components/Title/styles.ts +++ b/src/app/components/Title/Create/styles.ts @@ -46,6 +46,10 @@ export const ListItem = styled.div` color: ${props => tint(0.2, props.theme.colors.secondary.dark)} } } + + span { + width: 40%; + } `; export const Row = styled.div` diff --git a/src/app/components/Title/List/index.tsx b/src/app/components/Title/List/index.tsx new file mode 100644 index 0000000..6055389 --- /dev/null +++ b/src/app/components/Title/List/index.tsx @@ -0,0 +1,58 @@ +import React, { useEffect, useState } from 'react'; +import Button from '../../Button'; + +import { Container } from './styles'; +import { trigger } from '../../../util/EventHandler'; +import { AppEvent } from '../../../../common/AppEvent'; +import { Title } from '../Title'; + +const TitleList: React.FC = () => { + const [items, setItems] = useState([]); + + useEffect(() => { + const result = window.api.sendSync('listTitle', { + entity: 'Title', + }) as Title[]; + setItems(result); + }, []); + + const handleUpdate = (item: Title): void => { + trigger(AppEvent.titleTab, { action: 'update', value: item }); + }; + + const handleRead = (item: Title): void => { + trigger(AppEvent.titleTab, { action: 'read', value: item }); + }; + + return ( + <Container> + <> + <table> + <thead> + <tr> + <th>Name</th> + <th>ISBN</th> + <th>Actions</th> + </tr> + </thead> + <tbody> + {items.map((item, index) => { + return ( + <tr key={index}> + <td>{item.name}</td> + <td>{item.ISBN}</td> + <td> + <Button onClick={() => handleRead(item)}>Ver</Button> + <Button onClick={() => handleUpdate(item)}>Editar</Button> + </td> + </tr> + ); + })} + </tbody> + </table> + </> + </Container> + ); +}; + +export default TitleList; diff --git a/src/app/components/Title/List/styles.ts b/src/app/components/Title/List/styles.ts new file mode 100644 index 0000000..384808a --- /dev/null +++ b/src/app/components/Title/List/styles.ts @@ -0,0 +1,5 @@ +import styled from 'styled-components'; + +export const Container = styled.div` + +`; diff --git a/src/app/components/Title/Read/index.tsx b/src/app/components/Title/Read/index.tsx new file mode 100644 index 0000000..bcda43a --- /dev/null +++ b/src/app/components/Title/Read/index.tsx @@ -0,0 +1,134 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import Card from '../../Card'; +import { Title } from '../Title'; +import i18n from '../../../i18n'; + +import SectionContent from '../../Sections/SectionContent'; +import SectionHeader from '../../Sections/SectionHeader'; + +import { Container, List, ListItem, Row } from './styles'; +import { format, parseISO } from 'date-fns'; +import { v4 } from 'uuid'; +import Barcode from '../../Barcode'; + +interface SelectType { + id: string; + name: string; +} + +interface Publisher { + publisher: SelectType; + classification: string; + edition: string; + publishedAt: Date; +} + +const ReadTitle: React.FC<{ item: Title }> = ({ item }) => { + const sections = useMemo(() => ['editions', 'authors', 'categories'], []); + const [selectedSection, setSelectedSection] = useState('editions'); + + const [publishers, setPublishers] = useState<Publisher[]>([]); + const [authors, setAuthors] = useState<SelectType[]>([]); + const [categories, setCategories] = useState<SelectType[]>([]); + + useEffect(() => { + if (item !== undefined) { + const publishersAux: Publisher[] = item.titlePublishers.map((item) => ({ + id: item.id, + publisher: item.publisher, + classification: item.classification, + edition: item.edition, + publishedAt: parseISO(item.publishedAt.toString()), + })); + setPublishers(publishersAux); + + const authorsAux: SelectType[] = item.titleAuthors.map((item) => ({ + id: item.authorId.toString(), + name: item.author.name + })); + setAuthors(authorsAux); + + const categoriesAux: SelectType[] = item.titleCategories.map((item) => ({ + id: item.categoryId.toString(), + name: item.category.name, + })) + setCategories(categoriesAux); + } + }, [item]); + + return ( + <Container> + <Card title={item.name}> + <Barcode value={item.ISBN} options={{ + format: 'EAN13', + font: 'Monospace', + fontSize: 14, + textMargin: 0, + width: 1.5, + height: 54, + margin: 8, + textAlign: 'center', + background: '#FFFFFF', + lineColor: '#000000', + }} /> + </Card> + <Card> + <SectionHeader> + {sections.map((section) => ( + <a + key={section} + className={selectedSection === section ? 'active' : ''} + onClick={() => setSelectedSection(section)} + > + {i18n.t(`title.${section}`)} + </a> + ))} + </SectionHeader> + <div> + <SectionContent isActive={selectedSection === 'editions'}> + <Row> + <List> + {publishers.map((publisher) => ( + <ListItem key={v4()}> + <span>{publisher.classification}</span> + <span>{publisher.edition}</span> + <span> + {format(publisher.publishedAt, 'dd/MM/yyyy')} + </span> + <span>{publisher.publisher.name}</span> + </ListItem> + ))} + </List> + </Row> + </SectionContent> + + <SectionContent isActive={selectedSection === 'authors'}> + <Row> + <List> + {authors.map((author) => ( + <ListItem key={v4()}> + {author.name}{' '} + </ListItem> + ))} + </List> + </Row> + </SectionContent> + + <SectionContent isActive={selectedSection === 'categories'}> + <Row> + <List> + {categories.map((category) => ( + <ListItem key={v4()}> + {category.name}{' '} + </ListItem> + ))} + </List> + </Row> + </SectionContent> + </div> + </Card> + </Container> + ); +}; + +export default ReadTitle; diff --git a/src/app/components/Title/Read/styles.ts b/src/app/components/Title/Read/styles.ts new file mode 100644 index 0000000..796d1fd --- /dev/null +++ b/src/app/components/Title/Read/styles.ts @@ -0,0 +1,59 @@ +import { shade, tint } from 'polished'; +import styled, { css } from 'styled-components'; + +export const Container = styled.div` + padding: 8px; + width: 100%; +`; + +export const List = styled.div` + display: flex; + flex-direction: column; + width: 100%; +`; + +export const ListItem = styled.div` + display: flex; + flex-direction: row; + border-radius: 8px; + + justify-content: space-between; + align-items: center; + + padding: 12px; + margin-bottom: 4px; + width: 100%; + + background-color: ${props => tint(0.2, props.theme.colors.background)}; + &:hover { + background-color: ${props => tint(0.25, props.theme.colors.background)}; + ${(props) => props.theme.title == 'light' && css` + background-color: ${(props) => shade(0.05, props.theme.colors.background)}; + `} + } + + svg { + cursor: pointer; + &:hover { + color: ${props => tint(0.2, props.theme.colors.secondary.dark)} + } + } + + span { + width: 40%; + } +`; + +export const Row = styled.div` + padding: 24px 0 24px 0; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + width: 100%; +`; + +export const Column = styled.div` + display: flex; +`; diff --git a/src/app/components/Title/Title.ts b/src/app/components/Title/Title.ts new file mode 100644 index 0000000..fbf5050 --- /dev/null +++ b/src/app/components/Title/Title.ts @@ -0,0 +1,44 @@ +interface Author { + id: string; + name: string; +} + +interface TitleAuthor { + id: string; + authorId: number; + author: Author; +} + +interface Category { + id: string; + name: string; +} + +interface TitleCategory { + id: string; + categoryId: number; + category: Category; +} + +interface Publisher { + id: string; + name: string; +} + +interface TitlePublisher { + id: string; + edition: string; + classification: string; + publishedAt: Date; + publisherId: number; + publisher: Publisher; +} + +export interface Title { + id: string; + name: string; + ISBN: string; + titlePublishers: TitlePublisher[]; + titleAuthors: TitleAuthor[]; + titleCategories: TitleCategory[]; +} diff --git a/src/app/components/Title/Update/index.tsx b/src/app/components/Title/Update/index.tsx new file mode 100644 index 0000000..c3f635e --- /dev/null +++ b/src/app/components/Title/Update/index.tsx @@ -0,0 +1,440 @@ +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { format, parseISO } from 'date-fns'; +import { FiPlus, FiSave, FiTrash2 } from 'react-icons/fi'; +import { v4 } from 'uuid'; +import { useToast } from '../../../hooks/toast'; +import i18n from '../../../i18n'; +import AuthorSelect from '../../AuthorSelect'; +import Button from '../../Button'; +import CategorySelect from '../../CategorySelect'; +import { SelectHandles } from '../../CreatableSelectInput'; + +import Input from '../../Input'; +import PublisherSelect from '../../PublisherSelect'; +import SectionContent from '../../Sections/SectionContent'; +import SectionHeader from '../../Sections/SectionHeader'; + +import { ButtonContainer, Container, List, ListItem, Row } from './styles'; +import { Title } from '../Title'; + +interface SelectType { + id: string; + name: string; +} + +interface Publisher { + id?: string; + publisher: SelectType; + classification: string; + edition: string; + publishedAt: Date; +} + +const TitleUpdate: React.FC<{ item: Title }> = ({ item }) => { + const { addToast } = useToast(); + + const [title, setTitle] = useState(''); + const [isbn, setIsbn] = useState(''); + + const [selectedSection, setSelectedSection] = useState('editions'); + + const sections = useMemo(() => ['editions', 'authors', 'categories'], []); + + const [classification, setClassification] = useState(''); + const [edition, setEdition] = useState(''); + const [publishedAt, setPublishedAt] = useState(''); + const [publishers, setPublishers] = useState<Publisher[]>([]); + const refPublisher = useRef<SelectHandles>(null); + + const [authors, setAuthors] = useState<SelectType[]>([]); + const refAuthor = useRef<SelectHandles>(null); + + const [categories, setCategories] = useState<SelectType[]>([]); + const refCategory = useRef<SelectHandles>(null); + + useEffect(() => { + if (item !== undefined) { + setTitle(item.name); + setIsbn(item.ISBN); + + const publishersAux: Publisher[] = item.titlePublishers.map((item) => ({ + id: item.id, + publisher: item.publisher, + classification: item.classification, + edition: item.edition, + publishedAt: parseISO(item.publishedAt.toString()), + })); + setPublishers(publishersAux); + + const authorsAux: SelectType[] = item.titleAuthors.map((item) => ({ + id: item.authorId.toString(), + name: item.author.name + })); + setAuthors(authorsAux); + + const categoriesAux: SelectType[] = item.titleCategories.map((item) => ({ + id: item.categoryId.toString(), + name: item.category.name, + })) + setCategories(categoriesAux); + } + }, [item]); + + const handleAddPublisher = useCallback(() => { + const publisher = refPublisher.current.getValue<SelectType>(); + const errors: string[] = []; + if (!publisher) { + errors.push(i18n.t('publisher.label')); + } + if (!classification) { + errors.push(i18n.t('title.classification')); + } + if (!edition) { + errors.push(i18n.t('title.edition')); + } + if (!publishedAt) { + errors.push(i18n.t('title.publishedAt')); + } + if (errors.length > 0) { + addToast({ + title: i18n.t('notifications.warning'), + type: 'error', + description: i18n + .t('title.informErrorsEdition') + .replace('#errors#', errors.join(', ')), + }); + return; + } + + if ( + publishers.filter((a) => a.publisher.name === publisher.name).length > 0 + ) { + addToast({ + title: i18n.t('notifications.warning'), + type: 'error', + description: `${publisher.name} ${i18n.t('title.hasBeenAdd')}`, + }); + return; + } + + const pub = { + classification, + edition, + publishedAt: parseISO(publishedAt), + publisher, + }; + setPublishers((oldState) => [...oldState, pub]); + + refPublisher.current.clear(); + setClassification(''); + setEdition(''); + setPublishedAt(''); + }, [addToast, classification, edition, publishedAt, publishers]); + + const handleRemovePublisher = useCallback( + (publisher: Publisher) => { + const publishersFiltered = publishers.filter((c) => c !== publisher); + setPublishers(publishersFiltered); + }, + [publishers] + ); + + const handleAddAuthor = useCallback(() => { + const author = refAuthor.current.getValue<SelectType>(); + if (!author) { + addToast({ + title: i18n.t('notifications.warning'), + type: 'error', + description: i18n.t('title.selectAuthorToAdd'), + }); + return; + } + + if (authors.filter((a) => a.name === author.name).length > 0) { + addToast({ + title: i18n.t('notifications.warning'), + type: 'error', + description: `${author.name} ${i18n.t('title.hasBeenAdd')}`, + }); + return; + } + + setAuthors((oldState) => [...oldState, author]); + refAuthor.current.clear(); + }, [addToast, authors]); + + const handleRemoveAuthor = useCallback( + (author: SelectType) => { + const authorsFiltered = authors.filter((a) => a !== author); + setAuthors(authorsFiltered); + }, + [authors] + ); + + const handleAddCategory = useCallback(() => { + const category = refCategory.current.getValue<SelectType>(); + + if (!category) { + addToast({ + title: i18n.t('notifications.warning'), + type: 'error', + description: i18n.t('title.selectCategoryToAdd'), + }); + return; + } + + if (categories.filter((a) => a.name === category.name).length > 0) { + addToast({ + title: i18n.t('notifications.warning'), + type: 'error', + description: `${category.name} ${i18n.t('title.hasBeenAdd')}`, + }); + return; + } + + setCategories((oldState) => [...oldState, category]); + refCategory.current.clear(); + }, [addToast, categories]); + + const handleRemoveCategory = useCallback( + (category: SelectType) => { + const categoriesFiltered = categories.filter((c) => c !== category); + setCategories(categoriesFiltered); + }, + [categories] + ); + + const handleSave = useCallback(() => { + console.log(item); + + window.api.sendSync('update', { + entity: 'Title', + value: { + id: item.id, + name: title, + ISBN: isbn, + }, + }); + + ['TitleCategory', 'TitleAuthor', 'TitlePublisher'].map(tableName => { + window.api.sendSync('delete', { + entity: tableName, + value: { + titleId: item.id, + }, + }); + }); + + categories.map((category) => { + window.api.sendSync('create', { + entity: 'TitleCategory', + value: { + titleId: item.id, + categoryId: category.id, + }, + }); + }); + + authors.map((author) => { + window.api.sendSync('create', { + entity: 'TitleAuthor', + value: { + titleId: item.id, + authorId: author.id, + }, + }); + }); + + publishers.map((edition) => { + window.api.sendSync('create', { + entity: 'TitlePublisher', + value: { + titleId: item.id, + publisherId: edition.publisher.id, + edition: edition.edition, + classification: edition.classification, + publishedAt: edition.publishedAt, + }, + }); + }); + }, [authors, categories, isbn, item, publishers, title]); + + return ( + <Container> + <> + <div + style={{ + padding: '24px', + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + }} + > + <Input + containerStyle={{ flexGrow: 1, marginRight: '18px' }} + type="text" + name="name" + label={i18n.t('title.name')} + autoFocus + onChange={(e) => setTitle(e.target.value)} + value={title} + placeholder={i18n.t('title.typeTitleName')} + /> + + <Input + type="text" + name="ISBN" + label="ISBN" + value={isbn} + onChange={(e) => setIsbn(e.target.value)} + placeholder={i18n.t('title.typeISBN')} + /> + </div> + <SectionHeader> + {sections.map((section) => ( + <a + key={section} + className={selectedSection === section ? 'active' : ''} + onClick={() => setSelectedSection(section)} + > + {i18n.t(`title.${section}`)} + </a> + ))} + </SectionHeader> + <div> + <SectionContent isActive={selectedSection === 'editions'}> + <Row style={{ minHeight: '180px' }}> + <Input + type="text" + name="classification" + label={i18n.t('title.classification')} + placeholder={i18n.t('title.typeClassification')} + value={classification} + onChange={(e) => setClassification(e.target.value)} + /> +   + <Input + type="text" + name="edition" + label={i18n.t('title.edition')} + placeholder={i18n.t('title.typeEdition')} + value={edition} + onChange={(e) => setEdition(e.target.value)} + /> +   + <Input + type="date" + name="edition_date" + label={i18n.t('title.publishedAt')} + placeholder={i18n.t('title.typePublicationDate')} + alt={i18n.t('title.typePublicationDate')} + value={publishedAt} + onChange={(e) => setPublishedAt(e.target.value)} + /> +   + <PublisherSelect + ref={refPublisher} + containerStyle={{ flexGrow: 2 }} + /> +   + <Button style={{}} color="primary" onClick={handleAddPublisher}> + <FiPlus size={20} /> + </Button> + </Row> + <Row> + <List> + {publishers.map((publisher) => ( + <ListItem key={v4()}> + <span>{publisher.classification}</span> + <span>{publisher.edition}</span> + <span> + {format(publisher.publishedAt, 'dd/MM/yyyy')} + </span> + <span>{publisher.publisher.name}</span> + <span style={{ width: '10%', textAlign: 'right' }}> + <FiTrash2 + size={20} + onClick={() => handleRemovePublisher(publisher)} + title={i18n.t('title.removeEditionInformation')} + /> + </span> + </ListItem> + ))} + </List> + </Row> + </SectionContent> + + <SectionContent isActive={selectedSection === 'authors'}> + <Row> + <AuthorSelect + ref={refAuthor} + containerStyle={{ flexGrow: 2, marginRight: '16px' }} + /> + <Button color="primary" onClick={handleAddAuthor}> + <FiPlus size={20} /> + </Button> + </Row> + <Row> + <List> + {authors.map((author) => ( + <ListItem key={v4()}> + {author.name}{' '} + <FiTrash2 + size={20} + onClick={() => handleRemoveAuthor(author)} + title={i18n.t('title.removeAuthorInformation')} + /> + </ListItem> + ))} + </List> + </Row> + </SectionContent> + + <SectionContent isActive={selectedSection === 'categories'}> + <Row> + <CategorySelect + ref={refCategory} + containerStyle={{ flexGrow: 2, marginRight: '16px' }} + /> + + <Button color="primary" onClick={handleAddCategory}> + <FiPlus size={20} /> + </Button> + </Row> + <Row> + <List> + {categories.map((category) => ( + <ListItem key={v4()}> + {category.name}{' '} + <FiTrash2 + size={20} + onClick={() => handleRemoveCategory(category)} + title={i18n.t('title.removeCategoryInformation')} + /> + </ListItem> + ))} + </List> + </Row> + </SectionContent> + </div> + <ButtonContainer> + <Button + color="primary" + title={i18n.t('button.save')} + onClick={handleSave} + > + <FiSave size={20} /> + </Button> + </ButtonContainer> + </> + </Container> + ); +}; + +export default TitleUpdate; diff --git a/src/app/components/Title/Update/styles.ts b/src/app/components/Title/Update/styles.ts new file mode 100644 index 0000000..ad4c1d2 --- /dev/null +++ b/src/app/components/Title/Update/styles.ts @@ -0,0 +1,71 @@ +import { shade, tint } from 'polished'; +import styled, { css } from 'styled-components'; + +export const Container = styled.div` + display: flex; + flex-direction: column; + width: 100vw; +`; + + export const ButtonContainer = styled.div` + position: relative,; + padding: 0 24px; + display: flex; + justify-content: end; + `; + +export const List = styled.div` + display: flex; + flex-direction: column; + width: 100%; +`; + +export const ListItem = styled.div` + display: flex; + flex-direction: row; + border-radius: 8px; + + justify-content: space-between; + align-items: center; + + padding: 12px; + margin-bottom: 4px; + width: 100%; + + background-color: ${props => tint(0.2, props.theme.colors.background)}; + &:hover { + background-color: ${props => tint(0.25, props.theme.colors.background)}; + ${(props) => props.theme.title == 'light' && css` + background-color: ${(props) => shade(0.05, props.theme.colors.background)}; + `} + } + + svg { + cursor: pointer; + &:hover { + color: ${props => tint(0.2, props.theme.colors.secondary.dark)} + } + } + + span { + width: 40%; + overflow: hidden; + display: inline-block; + text-overflow:ellipsis; + white-space: nowrap; + } +`; + +export const Row = styled.div` + padding: 24px 0 24px 0; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + width: 100%; +`; + +export const Column = styled.div` + display: flex; +`; diff --git a/src/app/components/Title/index.tsx b/src/app/components/Title/index.tsx index ead74ab..49d279c 100644 --- a/src/app/components/Title/index.tsx +++ b/src/app/components/Title/index.tsx @@ -1,391 +1,20 @@ -import React, { useCallback, useMemo, useRef, useState } from 'react'; -import { format, parseISO } from 'date-fns'; -import { FiPlus, FiSave, FiTrash2 } from 'react-icons/fi'; -import { v4 } from 'uuid'; -import { useToast } from '../../hooks/toast'; -import i18n from '../../i18n'; -import AuthorSelect from '../AuthorSelect'; -import Button from '../Button'; -import CategorySelect from '../CategorySelect'; -import { SelectHandles } from '../CreatableSelectInput'; - -import Input from '../Input'; -import PublisherSelect from '../PublisherSelect'; -import SectionContent from '../Sections/SectionContent'; -import SectionHeader from '../Sections/SectionHeader'; - -import { ButtonContainer, Container, List, ListItem, Row } from './styles'; - -interface SelectType { - id: string; - name: string; -} - -interface Publisher { - publisher: SelectType; - classification: string; - edition: string; - publishedAt: Date; -} - -const Title: React.FC = () => { - const { addToast } = useToast(); - - const [title, setTitle] = useState(''); - const [isbn, setIsbn] = useState(''); - - const [selectedSection, setSelectedSection] = useState('editions'); - const sections = useMemo(() => ['editions', 'authors', 'categories'], []); - - const [classification, setClassification] = useState(''); - const [edition, setEdition] = useState(''); - const [publishedAt, setPublishedAt] = useState(''); - const [publishers, setPublishers] = useState<Publisher[]>([]); - const refPublisher = useRef<SelectHandles>(null); - - const [authors, setAuthors] = useState<SelectType[]>([]); - const refAuthor = useRef<SelectHandles>(null); - - const [categories, setCategories] = useState<SelectType[]>([]); - const refCategory = useRef<SelectHandles>(null); - - const handleAddPublisher = useCallback(() => { - const publisher = refPublisher.current.getValue<SelectType>(); - const errors: string[] = []; - if (!publisher) { - errors.push(i18n.t('publisher.label')); - } - if (!classification) { - errors.push(i18n.t('title.classification')); - } - if (!edition) { - errors.push(i18n.t('title.edition')); - } - if (!publishedAt) { - errors.push(i18n.t('title.publishedAt')); - } - if (errors.length > 0) { - addToast({ - title: i18n.t('notifications.warning'), - type: 'error', - description: i18n - .t('title.informErrorsEdition') - .replace('#errors#', errors.join(', ')), - }); - return; - } - - if ( - publishers.filter((a) => a.publisher.name === publisher.name).length > 0 - ) { - addToast({ - title: i18n.t('notifications.warning'), - type: 'error', - description: `${publisher.name} ${i18n.t('title.hasBeenAdd')}`, - }); - return; - } - - const pub = { - classification, - edition, - publishedAt: parseISO(publishedAt), - publisher, - }; - setPublishers((oldState) => [...oldState, pub]); - - refPublisher.current.clear(); - setClassification(''); - setEdition(''); - setPublishedAt(''); - }, [addToast, classification, edition, publishedAt, publishers]); - - const handleRemovePublisher = useCallback( - (publisher: Publisher) => { - const publishersFiltered = publishers.filter((c) => c !== publisher); - setPublishers(publishersFiltered); - }, - [publishers] - ); - - const handleAddAuthor = useCallback(() => { - const author = refAuthor.current.getValue<SelectType>(); - if (!author) { - addToast({ - title: i18n.t('notifications.warning'), - type: 'error', - description: i18n.t('title.selectAuthorToAdd'), - }); - return; - } - - if (authors.filter((a) => a.name === author.name).length > 0) { - addToast({ - title: i18n.t('notifications.warning'), - type: 'error', - description: `${author.name} ${i18n.t('title.hasBeenAdd')}`, - }); - return; - } - - setAuthors((oldState) => [...oldState, author]); - refAuthor.current.clear(); - }, [addToast, authors]); - - const handleRemoveAuthor = useCallback( - (author: SelectType) => { - const authorsFiltered = authors.filter((a) => a !== author); - setAuthors(authorsFiltered); - }, - [authors] - ); - - const handleAddCategory = useCallback(() => { - const category = refCategory.current.getValue<SelectType>(); - - if (!category) { - addToast({ - title: i18n.t('notifications.warning'), - type: 'error', - description: i18n.t('title.selectCategoryToAdd'), - }); - return; - } - - if (categories.filter((a) => a.name === category.name).length > 0) { - addToast({ - title: i18n.t('notifications.warning'), - type: 'error', - description: `${category.name} ${i18n.t('title.hasBeenAdd')}`, - }); - return; - } - - setCategories((oldState) => [...oldState, category]); - refCategory.current.clear(); - }, [addToast, categories]); - - const handleRemoveCategory = useCallback( - (category: SelectType) => { - const categoriesFiltered = categories.filter((c) => c !== category); - setCategories(categoriesFiltered); - }, - [categories] - ); - - const handleSave = useCallback(() => { - const result = window.api.sendSync('create', { - entity: 'Title', - value: { - name: title, - ISBN: isbn, - }, - }) as { id: string }; - - categories.map((category) => { - window.api.sendSync('create', { - entity: 'TitleCategory', - value: { - titleId: result.id, - categoryId: category.id, - }, - }); - }); - - authors.map((author) => { - window.api.sendSync('create', { - entity: 'TitleAuthor', - value: { - titleId: result.id, - authorId: author.id, - }, - }); - }); - - publishers.map((edition) => { - window.api.sendSync('create', { - entity: 'TitlePublisher', - value: { - titleId: result.id, - publisherId: edition.publisher.id, - edition: edition.edition, - classification: edition.classification, - publishedAt: edition.publishedAt, - }, - }); - }); - }, [authors, categories, isbn, publishers, title]); - +import React from 'react'; +import TitleCreate from './Create'; +import ListTitle from './List'; +import ReadTitle from './Read'; +import { Title } from './Title'; +import TitleUpdate from './Update'; + +const Title: React.FC<{ action: string, item?: unknown }> = ({ action, item }) => { + const title = item as Title; + console.log('title: ', title); return ( - <Container> - <div - style={{ - padding: '24px', - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', - }} - > - <Input - containerStyle={{ flexGrow: 1, marginRight: '18px' }} - type="text" - name="name" - label={i18n.t('title.name')} - autoFocus - onChange={(e) => setTitle(e.target.value)} - value={title} - placeholder={i18n.t('title.typeTitleName')} - /> - - <Input - type="text" - name="ISBN" - label="ISBN" - value={isbn} - onChange={(e) => setIsbn(e.target.value)} - placeholder={i18n.t('title.typeISBN')} - /> - </div> - <SectionHeader> - {sections.map((section) => ( - <a - key={section} - className={selectedSection === section ? 'active' : ''} - onClick={() => setSelectedSection(section)} - > - {i18n.t(`title.${section}`)} - </a> - ))} - </SectionHeader> - <div> - <SectionContent isActive={selectedSection === 'editions'}> - <Row style={{ minHeight: '180px' }}> - <Input - type="text" - name="classification" - label={i18n.t('title.classification')} - placeholder={i18n.t('title.typeClassification')} - value={classification} - onChange={(e) => setClassification(e.target.value)} - /> -   - <Input - type="text" - name="edition" - label={i18n.t('title.edition')} - placeholder={i18n.t('title.typeEdition')} - value={edition} - onChange={(e) => setEdition(e.target.value)} - /> -   - <Input - type="date" - name="edition_date" - label={i18n.t('title.publishedAt')} - placeholder={i18n.t('title.typePublicationDate')} - alt={i18n.t('title.typePublicationDate')} - value={publishedAt} - onChange={(e) => setPublishedAt(e.target.value)} - /> -   - <PublisherSelect - ref={refPublisher} - containerStyle={{ flexGrow: 2 }} - /> -   - <Button style={{}} color="primary" onClick={handleAddPublisher}> - <FiPlus size={20} /> - </Button> - </Row> - <Row> - <List> - {publishers.map((publisher) => ( - <ListItem key={v4()}> - <span>{publisher.classification}</span> - <span>{publisher.edition}</span> - <span> - {/* {format(parseISO(publisher.publishedAt), "dd/MM/yyyy HH:mm'h'")} */} - {format(publisher.publishedAt, 'dd/MM/yyyy')} - </span> - <span>{publisher.publisher.name}</span> - <FiTrash2 - size={20} - onClick={() => handleRemovePublisher(publisher)} - title={i18n.t('title.removeEditionInformation')} - /> - </ListItem> - ))} - </List> - </Row> - </SectionContent> - - <SectionContent isActive={selectedSection === 'authors'}> - <Row> - <AuthorSelect - ref={refAuthor} - containerStyle={{ flexGrow: 2, marginRight: '16px' }} - /> - <Button color="primary" onClick={handleAddAuthor}> - <FiPlus size={20} /> - </Button> - </Row> - <Row> - <List> - {authors.map((author) => ( - <ListItem key={v4()}> - {author.name}{' '} - <FiTrash2 - size={20} - onClick={() => handleRemoveAuthor(author)} - title={i18n.t('title.removeAuthorInformation')} - /> - </ListItem> - ))} - </List> - </Row> - </SectionContent> - - <SectionContent isActive={selectedSection === 'categories'}> - <Row> - <CategorySelect - ref={refCategory} - containerStyle={{ flexGrow: 2, marginRight: '16px' }} - /> - - <Button color="primary" onClick={handleAddCategory}> - <FiPlus size={20} /> - </Button> - </Row> - <Row> - <List> - {categories.map((category) => ( - <ListItem key={v4()}> - {category.name}{' '} - <FiTrash2 - size={20} - onClick={() => handleRemoveCategory(category)} - title={i18n.t('title.removeCategoryInformation')} - /> - </ListItem> - ))} - </List> - </Row> - </SectionContent> - </div> - <ButtonContainer> - {/* <Button title={i18n.t('button.remove')}> - <FiTrash2 size={20} /> - </Button> -    */} - <Button - color="primary" - title={i18n.t('button.save')} - onClick={handleSave} - > - <FiSave size={20} /> - </Button> - </ButtonContainer> - </Container> + <> + {action === 'create' && <TitleCreate />} + {action === 'list' && <ListTitle />} + {action === 'read' && <ReadTitle item={title} />} + {action === 'update' && <TitleUpdate item={title} />} + </> ); }; diff --git a/src/app/hooks/useBarcode.ts b/src/app/hooks/useBarcode.ts new file mode 100644 index 0000000..526de02 --- /dev/null +++ b/src/app/hooks/useBarcode.ts @@ -0,0 +1,45 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useRef, useEffect } from 'react'; +import JsBarcode from 'jsbarcode'; + +interface Options { + format?: string; + width?: number; + height?: number; + displayValue?: boolean; + text?: string; + fontOptions?: string; + font?: string; + textAlign?: string; + textPosition?: string; + textMargin?: number; + fontSize?: number; + background?: string; + lineColor?: string; + margin?: number; + marginTop?: number; + marginBottom?: number; + marginLeft?: number; + marginRight?: number; + flat?: boolean; + valid?: (valid: boolean) => void; +} + +export interface BarcodeProps { + value: string; + options?: Options; +} + +export function useBarcode({ ...props }: BarcodeProps): any { + const inputRef = useRef(); + const { value, options } = props; + + useEffect(() => { + if (inputRef) { + const ref = inputRef as any; + JsBarcode(ref.current, value, options); + } + }, [value, options]); + + return { inputRef }; +} diff --git a/src/app/styles/global.ts b/src/app/styles/global.ts index fdcf5ef..01cea62 100644 --- a/src/app/styles/global.ts +++ b/src/app/styles/global.ts @@ -99,11 +99,14 @@ export default createGlobalStyle` } span{ + padding: 7px 10px; + cursor: pointer; + svg{ margin-right: 7px; margin-bottom: -5px; + cursor: pointer; } - padding: 7px 10px; } button{ diff --git a/src/app/util/AppEventHandler.ts b/src/app/util/AppEventHandler.ts index ac44303..0b41c27 100644 --- a/src/app/util/AppEventHandler.ts +++ b/src/app/util/AppEventHandler.ts @@ -29,8 +29,8 @@ export default class { } forward(event: string): void { - const emit = () => { - trigger(event); + const emit = (_ev: never, params: unknown) => { + trigger(event, params); } window.api.on(event, emit.bind(this)); } diff --git a/src/app/util/DefaultEntities.ts b/src/app/util/DefaultEntities.ts index f8e6f15..502dbe0 100644 --- a/src/app/util/DefaultEntities.ts +++ b/src/app/util/DefaultEntities.ts @@ -5,6 +5,7 @@ import i18n from '../i18n'; import { trigger } from './EventHandler'; import { AppEvent } from '../../common/AppEvent'; import Borrow from '../components/Borrow'; +import { Actions } from '../../common/Actions'; export interface SearchSource { name: string; @@ -22,6 +23,15 @@ export interface SearchSource { }; } +const actionCreate = { + action: Actions.create +} + +const actionList = { + action: Actions.list +} + + export const entities: SearchSource[] = [ { name: 'borrow', @@ -61,8 +71,14 @@ export const entities: SearchSource[] = [ }, }, action: { - onClick: () => console.log('pessoa action click'), - onPress: () => console.log('pessoa action press'), + onClick: (): void => { + trigger(AppEvent.quickSearch); + trigger(AppEvent.personTab); + }, + onPress: (): void => { + trigger(AppEvent.quickSearch); + trigger(AppEvent.personTab); + }, }, }, { @@ -74,16 +90,22 @@ export const entities: SearchSource[] = [ handler: { onClick: (): void => { trigger(AppEvent.quickSearch); - trigger(AppEvent.titleTab); + trigger(AppEvent.titleTab, actionList); }, onPress: (): void => { trigger(AppEvent.quickSearch); - trigger(AppEvent.titleTab); + trigger(AppEvent.titleTab, actionList); }, }, action: { - onClick: () => console.log('titulo action click'), - onPress: () => console.log('titulo action press'), + onClick: (): void => { + trigger(AppEvent.quickSearch); + trigger(AppEvent.titleTab, actionCreate); + }, + onPress: (): void => { + trigger(AppEvent.quickSearch); + trigger(AppEvent.titleTab, actionCreate); + }, }, }, { diff --git a/src/common/Actions.ts b/src/common/Actions.ts new file mode 100644 index 0000000..65a5fe9 --- /dev/null +++ b/src/common/Actions.ts @@ -0,0 +1,7 @@ +export enum Actions { + read = 'read', + delete = 'delete', + update = 'update', + list = 'list', + create = 'create', +} diff --git a/src/electron/Main.ts b/src/electron/Main.ts index d194732..3359105 100644 --- a/src/electron/Main.ts +++ b/src/electron/Main.ts @@ -19,6 +19,7 @@ import { AppEvent } from '../common/AppEvent'; import fs from 'fs'; import { entityMap } from './database/EntityMap'; import RepositoryBase from './database/repository/RepositoryBase'; +import TitleRepository from './database/repository/TitleRepository'; declare const MAIN_WINDOW_WEBPACK_ENTRY: string; declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: string; @@ -68,6 +69,10 @@ export default class Main { return Factory.make(this.connection, entity); } + private getCustomRepository(entity: string, repository: any): any { + return Factory.customMake(this.connection, entity, repository); + } + protected async createWindow(): Promise<void> { ipcMain.on('create', async (event, content: Event[]) => { const { value, entity } = content[0]; @@ -79,6 +84,11 @@ export default class Main { event.returnValue = await this.getRepository(entity).update(value); }); + ipcMain.on('softDelete', async (event, content: Event[]) => { + const { value, entity } = content[0]; + event.returnValue = await this.getRepository(entity).softDelete(value); + }); + ipcMain.on('delete', async (event, content: Event[]) => { const { value, entity } = content[0]; event.returnValue = await this.getRepository(entity).delete(value); @@ -94,6 +104,11 @@ export default class Main { event.returnValue = await this.getRepository(entity).list(value); }); + ipcMain.on('listTitle', async (event, content: Event[]) => { + const { value, entity } = content[0]; + event.returnValue = await this.getCustomRepository(entity, TitleRepository).listTitle(value); + }); + ipcMain.on('globalSearch', async (event, content: Event[]) => { event.returnValue = []; }); diff --git a/src/electron/Menu.ts b/src/electron/Menu.ts index 83891fc..9ca14c3 100644 --- a/src/electron/Menu.ts +++ b/src/electron/Menu.ts @@ -3,6 +3,7 @@ import { i18n } from 'i18next'; import path from 'path'; import { AppEvent } from '../common/AppEvent'; import fs from 'fs'; +import { Actions } from '../common/Actions'; type Language = { code: string; @@ -66,6 +67,10 @@ const createMenuTemplate = async ( submenu: languageMenu }; + const actionCreate = { + action: Actions.create + } + template.push({ label: i18n.t('menu.file.label'), submenu: [ @@ -74,7 +79,7 @@ const createMenuTemplate = async ( accelerator: process.platform === 'darwin' ? 'Cmd+B' : 'Ctrl+B', click: async() => { if (mainWindow) { - mainWindow.webContents.send(AppEvent.borrowTab); + mainWindow.webContents.send(AppEvent.borrowTab, actionCreate); } } }, @@ -83,7 +88,7 @@ const createMenuTemplate = async ( accelerator: process.platform === 'darwin' ? 'Cmd+P' : 'Ctrl+P', click: async() => { if (mainWindow) { - mainWindow.webContents.send(AppEvent.personTab); + mainWindow.webContents.send(AppEvent.personTab, actionCreate); } } }, @@ -92,7 +97,7 @@ const createMenuTemplate = async ( accelerator: process.platform === 'darwin' ? 'Cmd+T' : 'Ctrl+T', click: async() => { if (mainWindow) { - mainWindow.webContents.send(AppEvent.titleTab); + mainWindow.webContents.send(AppEvent.titleTab, actionCreate); } } }, diff --git a/src/electron/contracts/Repository.ts b/src/electron/contracts/Repository.ts index aefed86..60e5871 100644 --- a/src/electron/contracts/Repository.ts +++ b/src/electron/contracts/Repository.ts @@ -1,7 +1,9 @@ +import typeORM from 'typeorm'; + export interface Repository { create(content: unknown): any; update(content: unknown): any; - delete(content: unknown): any; + delete(condition: unknown): Promise<typeORM.DeleteResult>; read(content: unknown): any; list(content: unknown): any; } diff --git a/src/electron/database/EntityMap.ts b/src/electron/database/EntityMap.ts index 80498a3..9bd73ca 100644 --- a/src/electron/database/EntityMap.ts +++ b/src/electron/database/EntityMap.ts @@ -14,7 +14,7 @@ import { Publisher } from './models/Publisher.schema'; import { Region } from './models/Region.schema'; import { Settings } from './models/Settings.schema'; import { Title } from './models/Title.schema'; -import { TitleAuthor } from './models/TtitleAuthor.schema'; +import { TitleAuthor } from './models/TitleAuthor.schema'; import { TitleCategory } from './models/TitleCategory.schema'; import { TitlePublisher } from './models/TitlePublisher.schema'; import { UserType } from './models/UserType.schema'; diff --git a/src/electron/database/factory/index.ts b/src/electron/database/factory/index.ts index 4ffa87d..25fcaff 100644 --- a/src/electron/database/factory/index.ts +++ b/src/electron/database/factory/index.ts @@ -7,6 +7,12 @@ class Factory { const dynamicRepository = new RepositoryBase(typeOrmRepository); return dynamicRepository; } + + static customMake(connection: Connection, model: string, repository: any): any { + const typeOrmRepository = connection.getRepository(model); + const dynamicRepository = new repository(typeOrmRepository); + return dynamicRepository; + } } export default Factory; diff --git a/src/electron/database/models/Author.schema.ts b/src/electron/database/models/Author.schema.ts index 8e00e80..67cafbb 100644 --- a/src/electron/database/models/Author.schema.ts +++ b/src/electron/database/models/Author.schema.ts @@ -10,6 +10,6 @@ export class Author @Column() name: string; - @OneToMany(() => Title, title => title.authors) + @OneToMany(() => Title, title => title.titleAuthors) titles: Title[]; } diff --git a/src/electron/database/models/Category.schema.ts b/src/electron/database/models/Category.schema.ts index 7455d55..0eccdfa 100644 --- a/src/electron/database/models/Category.schema.ts +++ b/src/electron/database/models/Category.schema.ts @@ -10,6 +10,6 @@ export class Category @Column() name: string; - @OneToMany(() => Title, title => title.categories) + @OneToMany(() => Title, title => title.titleCategories) titles: Title[]; } diff --git a/src/electron/database/models/Title.schema.ts b/src/electron/database/models/Title.schema.ts index 8580339..fea7239 100644 --- a/src/electron/database/models/Title.schema.ts +++ b/src/electron/database/models/Title.schema.ts @@ -1,7 +1,7 @@ import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'; -import { Category } from './Category.schema'; -import { Author } from './Author.schema'; import { TitlePublisher } from './TitlePublisher.schema'; +import { TitleCategory } from './TitleCategory.schema'; +import { TitleAuthor } from './TitleAuthor.schema'; @Entity() export class Title @@ -15,11 +15,11 @@ export class Title @Column() ISBN: string; - @OneToMany(() => Category, category => category.titles) - categories: Category[]; + @OneToMany(() => TitleCategory, titleCategory => titleCategory.title) + titleCategories: TitleCategory[]; - @OneToMany(() => Author, author => author.titles) - authors: Author[]; + @OneToMany(() => TitleAuthor, titleAuthor => titleAuthor.title) + titleAuthors: TitleAuthor[]; @OneToMany(() => TitlePublisher, titlePublisher => titlePublisher.title) titlePublishers: TitlePublisher[]; diff --git a/src/electron/database/models/TtitleAuthor.schema.ts b/src/electron/database/models/TitleAuthor.schema.ts similarity index 87% rename from src/electron/database/models/TtitleAuthor.schema.ts rename to src/electron/database/models/TitleAuthor.schema.ts index 2d235e3..4d24287 100644 --- a/src/electron/database/models/TtitleAuthor.schema.ts +++ b/src/electron/database/models/TitleAuthor.schema.ts @@ -14,7 +14,7 @@ export class TitleAuthor @Column() authorId: number; - @ManyToOne(() => Title, title => title.authors) + @ManyToOne(() => Title, title => title.titleAuthors) title: Title; @ManyToOne(() => Author, author => author.titles) diff --git a/src/electron/database/models/TitleCategory.schema.ts b/src/electron/database/models/TitleCategory.schema.ts index 26044c0..81be428 100644 --- a/src/electron/database/models/TitleCategory.schema.ts +++ b/src/electron/database/models/TitleCategory.schema.ts @@ -14,7 +14,7 @@ export class TitleCategory @Column() categoryId: number; - @ManyToOne(() => Title, title => title.categories) + @ManyToOne(() => Title, title => title.titleCategories) title: Title; @ManyToOne(() => Category, category => category.titles) diff --git a/src/electron/database/repository/RepositoryBase.ts b/src/electron/database/repository/RepositoryBase.ts index c77f4ff..8581503 100644 --- a/src/electron/database/repository/RepositoryBase.ts +++ b/src/electron/database/repository/RepositoryBase.ts @@ -2,7 +2,7 @@ import { Repository } from '../../contracts/Repository'; import typeORM from 'typeorm'; export default class RepositoryBase implements Repository { - private repository; + protected repository; constructor(typeOrm: typeORM.Repository<unknown>) { this.repository = typeOrm; @@ -28,7 +28,7 @@ export default class RepositoryBase implements Repository { } } - public async delete(content: unknown): Promise<unknown | unknown[]> { + public async softDelete(content: unknown): Promise<unknown | unknown[]> { try { const item = await this.repository.create(content); await this.repository.softRemove(item); @@ -39,6 +39,15 @@ export default class RepositoryBase implements Repository { } } + public async delete(condition: unknown): Promise<typeORM.DeleteResult> { + try { + return await this.repository.delete(condition); + } catch (err) { + console.log(err); + throw err; + } + } + public async read(content: unknown): Promise<unknown | unknown[]> { try { return await this.repository.find({ where: { id: content } }); diff --git a/src/electron/database/repository/TitleRepository.ts b/src/electron/database/repository/TitleRepository.ts new file mode 100644 index 0000000..3bf911e --- /dev/null +++ b/src/electron/database/repository/TitleRepository.ts @@ -0,0 +1,27 @@ +import RepositoryBase from './RepositoryBase'; + +export default class TitleRepository extends RepositoryBase { + public async listTitle(content: unknown): Promise<unknown | unknown[]> { + try { + let filter = { + relations: [ + 'titleAuthors', + 'titleAuthors.author', + 'titleCategories', + 'titleCategories.category', + 'titlePublishers', + 'titlePublishers.publisher', + ], + }; + + if (content) { + filter = { ...filter, ...{ where: content } }; + } + + return await this.repository.find(filter); + } catch (err) { + console.log(err); + throw err; + } + } +} diff --git a/src/locales/en-US/common.json b/src/locales/en-US/common.json index ff7f825..a7258f3 100644 --- a/src/locales/en-US/common.json +++ b/src/locales/en-US/common.json @@ -82,7 +82,10 @@ "removeCategoryInformation": "Remove category from list" }, "settings": { - "label": "Settings" + "label": "Settings", + "time": "Amount of days since borrow that reader must return book", + "path": "Path where backup will be done", + "saved": "Your settings have been saved" }, "notifications": { "information": "Information", diff --git a/src/locales/pt-BR/common.json b/src/locales/pt-BR/common.json index 17dd6a7..e1897f8 100644 --- a/src/locales/pt-BR/common.json +++ b/src/locales/pt-BR/common.json @@ -82,7 +82,10 @@ "removeCategoryInformation": "Remover categoria da lista" }, "settings": { - "label": "Configurações" + "label": "Configurações", + "time": "Quantidade de dias a partir do emprestimo em que o leitor deve devolver o livro", + "path": "Local onde será feito o backup das informações do sistema", + "saved": "Suas configurações foram salvas" }, "notifications": { "information": "Informação", diff --git a/yarn.lock b/yarn.lock index 2f037a4..2d8f4f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4864,6 +4864,11 @@ js-yaml@^4.0.0: dependencies: argparse "^2.0.1" +jsbarcode@^3.11.5: + version "3.11.5" + resolved "https://registry.yarnpkg.com/jsbarcode/-/jsbarcode-3.11.5.tgz#390b3efd0271f35b9d68c7b8af6e972445969014" + integrity sha512-zv3KsH51zD00I/LrFzFSM6dst7rDn0vIMzaiZFL7qusTjPZiPtxg3zxetp0RR7obmjTw4f6NyGgbdkBCgZUIrA== + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"