├── .eslintrc.js ├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── README.md ├── doc └── images │ ├── absences-card.png │ ├── averages-card.png │ ├── delays-card.png │ ├── evaluations-card.png │ ├── grades-card.png │ ├── homework-card.png │ └── timetable-card.png ├── elements ├── formfield.js ├── ignore │ ├── select.js │ ├── switch.js │ └── textfield.js ├── select.js ├── switch.js └── textfield.js ├── hacs.json ├── package.json ├── rollup-plugins └── ignore.js ├── rollup.config.dev.js ├── rollup.config.js ├── src ├── cards │ ├── absences-card.js │ ├── averages-card.js │ ├── base-card.js │ ├── base-period-related-card.js │ ├── delays-card.js │ ├── evaluations-card.js │ ├── grades-card.js │ ├── homework-card.js │ └── timetable-card.js ├── editors │ ├── absences-card-editor.js │ ├── averages-card-editor.js │ ├── base-editor.js │ ├── delays-card-editor.js │ ├── evaluations-card-editor.js │ ├── grades-card-editor.js │ ├── homework-card-editor.js │ └── timetable-card-editor.js └── pronote.ts ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", // Specifies the ESLint parser 3 | parserOptions: { 4 | ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features 5 | sourceType: "module" // Allows for the use of imports 6 | }, 7 | extends: [ 8 | "plugin:@typescript-eslint/recommended" // Uses the recommended rules from the @typescript-eslint/eslint-plugin 9 | ], 10 | rules: { 11 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs 12 | // e.g. "@typescript-eslint/explicit-function-return-type": "off", 13 | } 14 | }; -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: 'Build' 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | name: Test build 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v1 17 | - name: Build 18 | run: | 19 | npm install 20 | npm run build 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | release: 9 | name: Prepare release 10 | permissions: 11 | contents: write 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v1 15 | 16 | # Build 17 | - name: Build the file 18 | run: | 19 | npm install 20 | npm run build 21 | 22 | # Upload build file to the releas as an asset. 23 | - name: Upload built package to release 24 | uses: svenstaro/upload-release-action@v1-release 25 | with: 26 | repo_token: ${{ secrets.GITHUB_TOKEN }} 27 | file: dist/pronote.js 28 | asset_name: pronote.js 29 | tag: ${{ github.ref }} 30 | overwrite: true 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.rpt2_cache/ 3 | package-lock.json 4 | /dist 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lovelace cards for the Pronote integration 2 | 3 | A few cards to help display informations from the [Pronote integration for Home Assistant](https://github.com/delphiki/hass-pronote) 4 | 5 | ## Installation 6 | 7 | ### Using HACS 8 | 9 | Add this repository to HACS : https://github.com/delphiki/lovelace-pronote.git 10 | then: 11 | HACS > Lovelace > **Pronote Cards** 12 | 13 | ## Cards 14 | 15 | ### Timetable 16 | 17 | ![Timetable card example](/doc/images/timetable-card.png "Timetable card example"). 18 | 19 | ```yaml 20 | type: custom:pronote-timetable-card 21 | entity: sensor.pronote_XXXX_YYYY_timetable_next_day 22 | display_header: true 23 | display_lunch_break: true 24 | display_classroom: true 25 | display_teacher: true 26 | display_day_hours: true 27 | dim_ended_lessons: true 28 | max_days: null 29 | current_week_only: false 30 | ``` 31 | 32 | This card can be used with all timetable sensors. 33 | 34 | ### Homework 35 | 36 | ![Homework card example](/doc/images/homework-card.png "Homework card example"). 37 | 38 | ```yaml 39 | type: custom:pronote-homework-card 40 | entity: sensor.pronote_XXXX_YYYY_homework 41 | display_header: true 42 | display_done_homework: true 43 | reduce_done_homework: true 44 | current_week_only: false 45 | ``` 46 | 47 | This card can be used with all homework sensors. 48 | 49 | ### Grades 50 | 51 | ![Grades card example](/doc/images/grades-card.png "Grades card example"). 52 | 53 | ```yaml 54 | type: custom:pronote-grades-card 55 | entity: sensor.pronote_XXXX_YYYY_grades 56 | grade_format: full # 'full' will display grade as "X/Y", 'short' will display "X" 57 | display_header: true 58 | display_date: true 59 | display_comment: true 60 | display_class_average: true 61 | compare_with_class_average: true 62 | compare_with_ratio: null # use a float number, e.g. '0.6' to compare with the grade / out_of ratio 63 | display_coefficient: true 64 | display_class_min: true 65 | display_class_max: true 66 | display_new_grade_notice: true 67 | max_grades: null 68 | ``` 69 | 70 | ### Averages 71 | 72 | ![Averages card example](/doc/images/averages-card.png "Averages card example"). 73 | 74 | ```yaml 75 | type: custom:pronote-averages-card 76 | entity: sensor.pronote_XXXX_YYYY_averages 77 | average_format: full # 'full' will display grade as "X/Y", 'short' will display "X" 78 | display_header: true 79 | compare_with_class_average: true 80 | compare_with_ratio: null # use a float number, e.g. '0.6' to compare with the grade / out_of ratio 81 | display_class_average: true 82 | display_class_min: true 83 | display_class_max: true 84 | ``` 85 | 86 | ### Evaluations 87 | 88 | ![Evaluations card example](/doc/images/evaluations-card.png "Evaluations card example"). 89 | 90 | ```yaml 91 | type: custom:pronote-evaluations-card 92 | entity: sensor.pronote_XXXX_YYYY_evaluations 93 | display_header: true 94 | display_description: true 95 | display_teacher: true 96 | display_date: true 97 | display_comment: true 98 | display_coefficient: true 99 | max_evaluations: null 100 | child_name: null 101 | ``` 102 | 103 | ### Absences 104 | 105 | ![Absences card example](/doc/images/absences-card.png "Absences card example"). 106 | 107 | ```yaml 108 | type: custom:pronote-absences-card 109 | entity: sensor.pronote_XXXX_YYYY_absences 110 | display_header: true 111 | max_absences: null 112 | child_name: null 113 | ``` 114 | 115 | ### Delays 116 | 117 | ![Absences card example](/doc/images/delays-card.png "Delays card example"). 118 | 119 | ```yaml 120 | type: custom:pronote-delays-card 121 | entity: sensor.pronote_XXXX_YYYY_delays 122 | display_header: true 123 | max_delays: null 124 | child_name: null 125 | ``` -------------------------------------------------------------------------------- /doc/images/absences-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delphiki/lovelace-pronote/72f221975e4c93af93be34f7ceb0fe6e82e03457/doc/images/absences-card.png -------------------------------------------------------------------------------- /doc/images/averages-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delphiki/lovelace-pronote/72f221975e4c93af93be34f7ceb0fe6e82e03457/doc/images/averages-card.png -------------------------------------------------------------------------------- /doc/images/delays-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delphiki/lovelace-pronote/72f221975e4c93af93be34f7ceb0fe6e82e03457/doc/images/delays-card.png -------------------------------------------------------------------------------- /doc/images/evaluations-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delphiki/lovelace-pronote/72f221975e4c93af93be34f7ceb0fe6e82e03457/doc/images/evaluations-card.png -------------------------------------------------------------------------------- /doc/images/grades-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delphiki/lovelace-pronote/72f221975e4c93af93be34f7ceb0fe6e82e03457/doc/images/grades-card.png -------------------------------------------------------------------------------- /doc/images/homework-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delphiki/lovelace-pronote/72f221975e4c93af93be34f7ceb0fe6e82e03457/doc/images/homework-card.png -------------------------------------------------------------------------------- /doc/images/timetable-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delphiki/lovelace-pronote/72f221975e4c93af93be34f7ceb0fe6e82e03457/doc/images/timetable-card.png -------------------------------------------------------------------------------- /elements/formfield.js: -------------------------------------------------------------------------------- 1 | import { FormfieldBase } from '@material/mwc-formfield/mwc-formfield-base.js'; 2 | import { styles as formfieldStyles } from '@material/mwc-formfield/mwc-formfield.css.js'; 3 | 4 | export const formfieldDefinition = { 5 | 'mwc-formfield': class extends FormfieldBase { 6 | static get styles() { 7 | return formfieldStyles; 8 | } 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /elements/ignore/select.js: -------------------------------------------------------------------------------- 1 | export const ignoreSelectFiles = [ 2 | '@material/mwc-ripple/mwc-ripple.js', 3 | '@material/mwc-menu/mwc-menu.js', 4 | '@material/mwc-menu/mwc-menu-surface.js', 5 | '@material/mwc-list/mwc-list.js', 6 | '@material/mwc-list/mwc-list-item.js', 7 | '@material/mwc-icon/mwc-icon.js', 8 | ]; 9 | -------------------------------------------------------------------------------- /elements/ignore/switch.js: -------------------------------------------------------------------------------- 1 | export const ignoreSwitchFiles = ['@material/mwc-ripple/mwc-ripple.js']; 2 | -------------------------------------------------------------------------------- /elements/ignore/textfield.js: -------------------------------------------------------------------------------- 1 | export const ignoreTextfieldFiles = ['@material/mwc-notched-outline/mwc-notched-outline.js']; 2 | -------------------------------------------------------------------------------- /elements/select.js: -------------------------------------------------------------------------------- 1 | import { SelectBase } from '@material/mwc-select/mwc-select-base.js'; 2 | import { ListBase } from '@material/mwc-list/mwc-list-base.js'; 3 | import { ListItemBase } from '@material/mwc-list/mwc-list-item-base.js'; 4 | import { MenuBase } from '@material/mwc-menu/mwc-menu-base.js'; 5 | import { MenuSurfaceBase } from '@material/mwc-menu/mwc-menu-surface-base.js'; 6 | import { RippleBase } from '@material/mwc-ripple/mwc-ripple-base.js'; 7 | import { NotchedOutlineBase } from '@material/mwc-notched-outline/mwc-notched-outline-base.js'; 8 | 9 | import { styles as selectStyles } from '@material/mwc-select/mwc-select.css'; 10 | import { styles as listStyles } from '@material/mwc-list/mwc-list.css'; 11 | import { styles as listItemStyles } from '@material/mwc-list//mwc-list-item.css'; 12 | import { styles as rippleStyles } from '@material/mwc-ripple/mwc-ripple.css'; 13 | import { styles as menuStyles } from '@material/mwc-menu/mwc-menu.css'; 14 | import { styles as menuSurfaceStyles } from '@material/mwc-menu/mwc-menu-surface.css'; 15 | import { styles as notchedOutlineStyles } from '@material/mwc-notched-outline/mwc-notched-outline.css'; 16 | 17 | export const selectDefinition = { 18 | 'mwc-select': class extends SelectBase { 19 | static get styles() { 20 | return selectStyles; 21 | } 22 | }, 23 | 'mwc-list': class extends ListBase { 24 | static get styles() { 25 | return listStyles; 26 | } 27 | }, 28 | 'mwc-list-item': class extends ListItemBase { 29 | static get styles() { 30 | return listItemStyles; 31 | } 32 | }, 33 | 'mwc-ripple': class extends RippleBase { 34 | static get styles() { 35 | return rippleStyles; 36 | } 37 | }, 38 | 'mwc-menu': class extends MenuBase { 39 | static get styles() { 40 | return menuStyles; 41 | } 42 | }, 43 | 'mwc-menu-surface': class extends MenuSurfaceBase { 44 | static get styles() { 45 | return menuSurfaceStyles; 46 | } 47 | }, 48 | 'mwc-notched-outline': class extends NotchedOutlineBase { 49 | static get styles() { 50 | return notchedOutlineStyles; 51 | } 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /elements/switch.js: -------------------------------------------------------------------------------- 1 | import { SwitchBase } from '@material/mwc-switch/deprecated/mwc-switch-base.js'; 2 | import { RippleBase } from '@material/mwc-ripple/mwc-ripple-base.js'; 3 | import { styles as switchStyles } from '@material/mwc-switch/deprecated/mwc-switch.css'; 4 | import { styles as rippleStyles } from '@material/mwc-ripple/mwc-ripple.css'; 5 | 6 | export const switchDefinition = { 7 | 'mwc-switch': class extends SwitchBase { 8 | static get styles() { 9 | return switchStyles; 10 | } 11 | }, 12 | 'mwc-ripple': class extends RippleBase { 13 | static get styles() { 14 | return rippleStyles; 15 | } 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /elements/textfield.js: -------------------------------------------------------------------------------- 1 | import { TextFieldBase } from '@material/mwc-textfield/mwc-textfield-base.js'; 2 | import { NotchedOutlineBase } from '@material/mwc-notched-outline/mwc-notched-outline-base.js'; 3 | 4 | import { styles as textfieldStyles } from '@material/mwc-textfield/mwc-textfield.css'; 5 | import { styles as notchedOutlineStyles } from '@material/mwc-notched-outline/mwc-notched-outline.css'; 6 | 7 | export const textfieldDefinition = { 8 | 'mwc-textfield': class extends TextFieldBase { 9 | static get styles() { 10 | return textfieldStyles; 11 | } 12 | }, 13 | 'mwc-notched-outline': class extends NotchedOutlineBase { 14 | static get styles() { 15 | return notchedOutlineStyles; 16 | } 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Pronote Cards", 3 | "filename": "pronote.js", 4 | "render_readme": true 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lovelace-pronote", 3 | "version": "1.0.1", 4 | "description": "Lovelace cards for Pronote", 5 | "main": "dist/pronote.js", 6 | "scripts": { 7 | "build": "npm run lint && npm run rollup", 8 | "rollup": "rollup -c", 9 | "babel": "babel dist/pronote.js --out-file dist/pronote.js", 10 | "lint": "eslint src/*.ts", 11 | "watch": "rollup -c --watch", 12 | "postversion": "npm run build", 13 | "audit-fix": "npx yarn-audit-fix" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/delphiki/lovelace-pronote.git" 18 | }, 19 | "keywords": [ 20 | "lovelace" 21 | ], 22 | "author": "delphiki", 23 | "contributors": [ 24 | "delphiki (https://github.com/delphiki)" 25 | ], 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/delphiki/lovelace-pronote/issues" 29 | }, 30 | "homepage": "https://github.com/delphiki/lovelace-pronote#readme", 31 | "devDependencies": { 32 | "@babel/core": "^7.12.3", 33 | "@babel/plugin-proposal-class-properties": "^7.12.1", 34 | "@babel/plugin-proposal-decorators": "^7.12.1", 35 | "@rollup/plugin-babel": "^5.2.1", 36 | "@rollup/plugin-commonjs": "^16.0.0", 37 | "@rollup/plugin-json": "^4.0.2", 38 | "@rollup/plugin-node-resolve": "^10.0.0", 39 | "@semantic-release/changelog": "^5.0.1", 40 | "@semantic-release/commit-analyzer": "^8.0.1", 41 | "@semantic-release/exec": "^5.0.0", 42 | "@semantic-release/git": "^9.0.0", 43 | "@semantic-release/npm": "^7.0.10", 44 | "@semantic-release/release-notes-generator": "^9.0.1", 45 | "@typescript-eslint/eslint-plugin": "^6.1.0", 46 | "@typescript-eslint/parser": "^6.1.0", 47 | "conventional-changelog-conventionalcommits": "^4.5.0", 48 | "eslint": "7.12.1", 49 | "eslint-config-airbnb-base": "^14.1.0", 50 | "eslint-config-prettier": "^6.15.0", 51 | "eslint-plugin-import": "^2.22.1", 52 | "eslint-plugin-prettier": "^3.1.2", 53 | "npm": "^6.14.3", 54 | "prettier": "^2.1.2", 55 | "prettier-eslint": "^11.0.0", 56 | "rollup": "^2.33.1", 57 | "rollup-plugin-cleanup": "^3.2.1", 58 | "rollup-plugin-serve": "^1.1.0", 59 | "rollup-plugin-terser": "^7.0.2", 60 | "rollup-plugin-typescript2": "^0.29.0", 61 | "semantic-release": "^17.3.8", 62 | "ts-lit-plugin": "^1.1.10", 63 | "typescript": "^4.0.5", 64 | "typescript-styled-plugin": "^0.15.0", 65 | "yarn-audit-fix": "^9.3.10" 66 | }, 67 | "dependencies": { 68 | "@ctrl/tinycolor": "^3.1.6", 69 | "@material/mwc-ripple": "^0.19.1", 70 | "fast-copy": "^2.1.0", 71 | "home-assistant-js-websocket": "^8.2.0", 72 | "lit": "^2.7.6", 73 | "lit-element": "^3.3.2", 74 | "lit-html": "^2.7.5", 75 | "memoize-one": "^6.0.0" 76 | } 77 | } -------------------------------------------------------------------------------- /rollup-plugins/ignore.js: -------------------------------------------------------------------------------- 1 | export default function (userOptions = {}) { 2 | // Files need to be absolute paths. 3 | // This only works if the file has no exports 4 | // and only is imported for its side effects 5 | const files = userOptions.files || []; 6 | 7 | if (files.length === 0) { 8 | return { 9 | name: 'ignore', 10 | }; 11 | } 12 | 13 | return { 14 | name: 'ignore', 15 | 16 | load(id) { 17 | return files.some((toIgnorePath) => id.startsWith(toIgnorePath)) 18 | ? { 19 | code: '', 20 | } 21 | : null; 22 | }, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /rollup.config.dev.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve'; 2 | import typescript from 'rollup-plugin-typescript2'; 3 | import babel from 'rollup-plugin-babel'; 4 | import serve from 'rollup-plugin-serve'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | import json from '@rollup/plugin-json'; 7 | import ignore from './rollup-plugins/ignore'; 8 | import { ignoreTextfieldFiles } from './elements/ignore/textfield'; 9 | import { ignoreSelectFiles } from './elements/ignore/select'; 10 | import { ignoreSwitchFiles } from './elements/ignore/switch'; 11 | 12 | export default { 13 | input: ['src/pronote.ts'], 14 | output: { 15 | dir: './dist', 16 | format: 'es', 17 | }, 18 | plugins: [ 19 | resolve(), 20 | typescript(), 21 | json(), 22 | babel({ 23 | exclude: 'node_modules/**', 24 | }), 25 | terser(), 26 | serve({ 27 | contentBase: './dist', 28 | host: '0.0.0.0', 29 | port: 5000, 30 | allowCrossOrigin: true, 31 | headers: { 32 | 'Access-Control-Allow-Origin': '*', 33 | }, 34 | }), 35 | ignore({ 36 | files: [...ignoreTextfieldFiles, ...ignoreSelectFiles, ...ignoreSwitchFiles].map((file) => require.resolve(file)), 37 | }), 38 | ], 39 | }; 40 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import nodeResolve from '@rollup/plugin-node-resolve'; 4 | import babel from '@rollup/plugin-babel'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | import serve from 'rollup-plugin-serve'; 7 | import json from '@rollup/plugin-json'; 8 | import cleanup from 'rollup-plugin-cleanup'; 9 | 10 | const dev = process.env.ROLLUP_WATCH; 11 | 12 | const serveopts = { 13 | contentBase: ['./dist'], 14 | host: '0.0.0.0', 15 | port: 5000, 16 | allowCrossOrigin: true, 17 | headers: { 18 | 'Access-Control-Allow-Origin': '*', 19 | }, 20 | }; 21 | 22 | const plugins = [ 23 | nodeResolve({}), 24 | commonjs(), 25 | typescript(), 26 | json(), 27 | babel({ 28 | exclude: 'node_modules/**', 29 | babelHelpers: 'bundled', 30 | }), 31 | cleanup({ comments: 'none' }), 32 | dev && serve(serveopts), 33 | !dev && 34 | terser({ 35 | mangle: { 36 | safari10: true, 37 | }, 38 | }), 39 | ]; 40 | 41 | export default [ 42 | { 43 | input: 'src/pronote.ts', 44 | output: { 45 | dir: './dist', 46 | format: 'es', 47 | sourcemap: dev ? true : false, 48 | }, 49 | plugins: [...plugins], 50 | watch: { 51 | exclude: 'node_modules/**', 52 | }, 53 | }, 54 | ]; -------------------------------------------------------------------------------- /src/cards/absences-card.js: -------------------------------------------------------------------------------- 1 | import BasePeriodRelatedPronoteCard from './base-period-related-card'; 2 | 3 | const LitElement = Object.getPrototypeOf( 4 | customElements.get("ha-panel-lovelace") 5 | ); 6 | const html = LitElement.prototype.html; 7 | const css = LitElement.prototype.css; 8 | 9 | class PronoteAbsencesCard extends BasePeriodRelatedPronoteCard { 10 | 11 | period_sensor_key = 'absences' 12 | items_attribute_key = 'absences' 13 | header_title = 'Absences de ' 14 | no_data_message = 'Aucune absence' 15 | 16 | getAbsencesRow(absence) { 17 | let from = this.getFormattedDate(absence.from); 18 | let to = this.getFormattedDate(absence.to); 19 | let content = html` 20 | 21 | 22 | ${absence.justified ? html`` : html``} 23 | 24 | 25 | ${from} au ${to}
${absence.hours} de cours manquées 26 | 27 | 28 | ${absence.reason} 29 | 30 | 31 | ` 32 | return html`${content}`; 33 | } 34 | 35 | getFormattedDate(date) { 36 | return (new Date(date)) ? new Date(date).toLocaleDateString('fr-FR', { weekday: 'long', day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }).replace(/^(.)/, (match) => match.toUpperCase()) : ''; 37 | } 38 | 39 | getCardContent() { 40 | const stateObj = this.hass.states[this.config.entity]; 41 | 42 | if (stateObj) { 43 | const absences = this.getFilteredItems(); 44 | const itemTemplates = [ 45 | this.getPeriodSwitcher() 46 | ]; 47 | let dayTemplates = []; 48 | let absencesCount = 0; 49 | for (let index = 0; index < absences.length; index++) { 50 | absencesCount++; 51 | if (this.config.max_absences && this.config.max_absences < absencesCount) { 52 | break; 53 | } 54 | let absence = absences[index]; 55 | dayTemplates.push(this.getAbsencesRow(absence)); 56 | } 57 | 58 | if (absencesCount > 0) { 59 | itemTemplates.push(html`${dayTemplates}
`); 60 | } else { 61 | itemTemplates.push(this.noDataMessage()); 62 | } 63 | 64 | return itemTemplates; 65 | } 66 | } 67 | 68 | getDefaultConfig() { 69 | return { 70 | ...super.getDefaultConfig(), 71 | display_header: true, 72 | max_absences: null 73 | }; 74 | } 75 | 76 | static get styles() { 77 | return css` 78 | ${super.styles} 79 | table{ 80 | clear:both; 81 | font-size: 0.9em; 82 | font-family: Roboto; 83 | width: 100%; 84 | outline: 0px solid #393c3d; 85 | border-collapse: collapse; 86 | } 87 | tr:nth-child(odd) { 88 | background-color: rgba(0,0,0,0.1); 89 | } 90 | td { 91 | vertical-align: middle; 92 | padding: 5px 10px 5px 10px; 93 | text-align: left; 94 | } 95 | tr td:first-child { 96 | width: 10%; 97 | text-align:right; 98 | } 99 | span.absence-reason { 100 | font-weight:bold; 101 | display:block; 102 | } 103 | tr td:nth-child(2) { 104 | width: 4px; 105 | padding: 5px 0; 106 | } 107 | tr td:nth-child(2) > span { 108 | display:inline-block; 109 | width: 4px; 110 | height: 3rem; 111 | border-radius:4px; 112 | background-color: grey; 113 | margin-top:4px; 114 | } 115 | span.absence-from { 116 | font-weight:bold; 117 | padding: 4px; 118 | border-radius: 4px; 119 | } 120 | span.absence-hours { 121 | font-size: 0.9em; 122 | padding: 4px; 123 | } 124 | table + div { 125 | border-top: 1px solid white; 126 | } 127 | `; 128 | } 129 | 130 | static getStubConfig() { 131 | return { 132 | display_header: true, 133 | max_absences: null, 134 | } 135 | } 136 | 137 | static getConfigElement() { 138 | return document.createElement("pronote-absences-card-editor"); 139 | } 140 | } 141 | 142 | customElements.define("pronote-absences-card", PronoteAbsencesCard); 143 | 144 | window.customCards = window.customCards || []; 145 | window.customCards.push({ 146 | type: "pronote-absences-card", 147 | name: "Pronote Absences Card", 148 | description: "Display the absences from Pronote", 149 | documentationURL: "https://github.com/delphiki/lovelace-pronote?tab=readme-ov-file#absences", 150 | }); 151 | -------------------------------------------------------------------------------- /src/cards/averages-card.js: -------------------------------------------------------------------------------- 1 | import BasePeriodRelatedPronoteCard from './base-period-related-card'; 2 | 3 | const LitElement = Object.getPrototypeOf( 4 | customElements.get("ha-panel-lovelace") 5 | ); 6 | const html = LitElement.prototype.html; 7 | const css = LitElement.prototype.css; 8 | 9 | class PronoteAveragesCard extends BasePeriodRelatedPronoteCard { 10 | 11 | period_sensor_key = 'averages' 12 | items_attribute_key = 'averages' 13 | header_title = 'Moyennes de ' 14 | no_data_message = 'Aucune moyenne' 15 | allow_all_periods = false 16 | 17 | getOverallAverageRow() { 18 | let sensor_prefix = this.config.entity.split('_'+this.period_sensor_key)[0]; 19 | let overall_average_entity = `${sensor_prefix}_overall_average`; 20 | 21 | if (this.period_filter !== null && !this.isCurrentPeriodSelected()) { 22 | overall_average_entity = `${overall_average_entity}_${this.period_filter}`; 23 | } 24 | 25 | if (!this.hass.states[overall_average_entity]) { 26 | return html``; 27 | } 28 | 29 | let overall_average = this.hass.states[overall_average_entity].state; 30 | 31 | if (!overall_average) { 32 | return html``; 33 | } 34 | 35 | let average = parseFloat(overall_average.replace(',', '.')); 36 | let average_classes = []; 37 | 38 | if (this.config.compare_with_ratio !== null) { 39 | let comparison_ratio = parseFloat(this.config.compare_with_ratio); 40 | let average_ratio = average / parseFloat(overall_average.out_of.replace(',', '.')); 41 | average_classes.push(average_ratio >= comparison_ratio ? 'above-ratio' : 'below-ratio'); 42 | } 43 | 44 | return html` 45 | 46 | 47 | 48 | Moyenne générale 49 | 50 | 51 | ${overall_average.replace('.', ',')} 52 | 53 | 54 | `; 55 | } 56 | 57 | getAverageRow(averageData) { 58 | let average = parseFloat(averageData.average.replace(',', '.')); 59 | 60 | let average_classes = []; 61 | 62 | if (this.config.compare_with_ratio !== null) { 63 | let comparison_ratio = parseFloat(this.config.compare_with_ratio); 64 | let average_ratio = average / parseFloat(averageData.out_of.replace(',', '.')); 65 | average_classes.push(average_ratio >= comparison_ratio ? 'above-ratio' : 'below-ratio'); 66 | } else if (this.config.compare_with_class_average && averageData.class) { 67 | let class_average = parseFloat(averageData.class.replace(',', '.')); 68 | average_classes.push(average > class_average ? 'above-average' : 'below-average'); 69 | } 70 | 71 | let formatted_average = averageData.average+'/'+averageData.out_of; 72 | if (this.config.average_format === 'short') { 73 | formatted_average = averageData.average; 74 | } 75 | 76 | return html` 77 | 78 | 79 | 80 | ${averageData.subject} 81 | 82 | 83 | ${formatted_average} 84 | ${this.config.display_class_average && averageData.class ? html`Classe ${averageData.class}` : ''} 85 | ${this.config.display_class_min && averageData.min ? html`Min. ${averageData.min}` : ''} 86 | ${this.config.display_class_max && averageData.max ? html`Max. ${averageData.max}` : ''} 87 | 88 | 89 | `; 90 | } 91 | 92 | getCardContent() { 93 | const stateObj = this.hass.states[this.config.entity]; 94 | 95 | if (stateObj) { 96 | const averages = this.getFilteredItems(); 97 | const itemTemplates = [ 98 | this.getPeriodSwitcher() 99 | ]; 100 | const averagesRows = []; 101 | 102 | if (this.config.display_overall_average) { 103 | averagesRows.push(this.getOverallAverageRow(averages)); 104 | } 105 | 106 | for (let index = 0; index < averages.length; index++) { 107 | let average = averages[index]; 108 | averagesRows.push(this.getAverageRow(average)); 109 | } 110 | 111 | if (averagesRows.length > 0) { 112 | itemTemplates.push(html`${averagesRows}
`); 113 | } else { 114 | itemTemplates.push(this.noDataMessage()); 115 | } 116 | 117 | return itemTemplates; 118 | } 119 | 120 | return []; 121 | } 122 | 123 | getDefaultConfig() { 124 | return { 125 | ...super.getDefaultConfig(), 126 | average_format: 'full', 127 | display_header: true, 128 | display_class_average: true, 129 | compare_with_class_average: true, 130 | compare_with_ratio: null, 131 | display_class_min: true, 132 | display_class_max: true, 133 | display_overall_average: true, 134 | } 135 | } 136 | 137 | static get styles() { 138 | return css` 139 | ${super.styles} 140 | table { 141 | font-size: 0.9em; 142 | font-family: Roboto; 143 | width: 100%; 144 | outline: 0px solid #393c3d; 145 | border-collapse: collapse; 146 | } 147 | td { 148 | vertical-align: top; 149 | padding: 5px 10px 5px 10px; 150 | padding-top: 8px; 151 | text-align: left; 152 | } 153 | tr.overall-average { 154 | border-bottom: 1px solid #393c3d; 155 | } 156 | tr.overall-average td { 157 | text-transform: uppercase; 158 | padding-bottom: 10px; 159 | } 160 | tr.overall-average .average-value span { 161 | padding: 5px; 162 | border-radius: 5px; 163 | border: 1px solid var(--primary-text-color); 164 | } 165 | tr.overall-average tr td { 166 | padding-top: 10px; 167 | } 168 | td.average-comparison-color, td.average-color { 169 | width: 4px; 170 | padding-top: 11px; 171 | } 172 | td.average-comparison-color > span, td.average-color > span { 173 | display:inline-block; 174 | width: 4px; 175 | height: 1rem; 176 | border-radius:4px; 177 | background-color: grey; 178 | } 179 | 180 | .above-average .average-detail, .above-ratio .average-detail, 181 | .below-average .average-detail, .below-ratio .average-detail { 182 | position: relative; 183 | } 184 | .above-average span.average-value, .above-ratio span.average-value, 185 | .below-average span.average-value, .below-ratio span.average-value { 186 | padding-right: 16px; 187 | } 188 | .above-average span.average-value:before, .above-ratio span.average-value:before, 189 | .below-average span.average-value:before, .below-ratio span.average-value:before { 190 | content: ' '; 191 | display: block; 192 | width: 10px; 193 | height: 10px; 194 | border-radius: 5px; 195 | position: absolute; 196 | right: 10px; 197 | top: 13px; 198 | } 199 | .above-average span.average-value:before, .above-ratio span.average-value:before { 200 | background-color: green; 201 | } 202 | .below-average span.average-value:before, .below-ratio span.average-value:before { 203 | background-color: orange; 204 | } 205 | .average-description { 206 | padding-left: 0; 207 | } 208 | .average-subject { 209 | display: inline-block; 210 | font-weight: bold; 211 | position: relative; 212 | } 213 | .average-detail { 214 | text-align: right; 215 | } 216 | .average-value { 217 | font-weight: bold; 218 | } 219 | .average-value, .average-class-average { 220 | display:block; 221 | } 222 | .average-class-average, .average-class-min, .average-class-max { 223 | font-size: 0.9em; 224 | color: gray; 225 | } 226 | .average-class-min + .average-class-max:before { 227 | content: ' - ' 228 | } 229 | `; 230 | } 231 | 232 | static getStubConfig() { 233 | return { 234 | average_format: 'full', 235 | display_header: true, 236 | display_class_average: true, 237 | compare_with_class_average: true, 238 | compare_with_ratio: null, 239 | display_class_min: true, 240 | display_class_max: true, 241 | } 242 | } 243 | 244 | static getConfigElement() { 245 | return document.createElement("pronote-averages-card-editor"); 246 | } 247 | } 248 | 249 | customElements.define("pronote-averages-card", PronoteAveragesCard); 250 | 251 | window.customCards = window.customCards || []; 252 | window.customCards.push({ 253 | type: "pronote-averages-card", 254 | name: "Pronote Averages Card", 255 | description: "Display the averages from Pronote", 256 | documentationURL: "https://github.com/delphiki/lovelace-pronote?tab=readme-ov-file#averages", 257 | }); -------------------------------------------------------------------------------- /src/cards/base-card.js: -------------------------------------------------------------------------------- 1 | const LitElement = Object.getPrototypeOf( 2 | customElements.get("ha-panel-lovelace") 3 | ); 4 | const html = LitElement.prototype.html; 5 | const css = LitElement.prototype.css; 6 | 7 | class BasePronoteCard extends LitElement { 8 | 9 | static get properties() { 10 | return { 11 | config: {}, 12 | hass: {}, 13 | header_title: { type: String }, 14 | no_data_message: { type: String } 15 | }; 16 | } 17 | 18 | getCardHeader() { 19 | let child_attributes = this.hass.states[this.config.entity].attributes; 20 | let child_name = (typeof child_attributes['nickname'] === 'string' && child_attributes['nickname'].length > 0) ? child_attributes['nickname'] : child_attributes['full_name']; 21 | return html`
${this.header_title} ${child_name}
`; 22 | } 23 | 24 | noDataMessage() { 25 | return html`
${this.no_data_message}
`; 26 | } 27 | 28 | render() { 29 | if (!this.config || !this.hass) { 30 | return html``; 31 | } 32 | 33 | const stateObj = this.hass.states[this.config.entity]; 34 | 35 | if (stateObj) { 36 | 37 | return html` 38 | 39 | ${this.config.display_header ? this.getCardHeader() : ''} 40 | ${this.getCardContent()} 41 | ` 42 | ; 43 | } 44 | } 45 | 46 | // Définit la configuration de la carte 47 | setConfig(config) { 48 | if (!config.entity) { 49 | throw new Error('You need to define an entity'); 50 | } 51 | 52 | this.config = { 53 | ...this.getDefaultConfig(), 54 | ...config 55 | }; 56 | } 57 | 58 | static get styles() { 59 | return css` 60 | .pronote-card-header { 61 | text-align:center; 62 | } 63 | div { 64 | padding: 12px; 65 | font-weight:bold; 66 | font-size:1em; 67 | } 68 | .pronote-card-no-data { 69 | display:block; 70 | padding:8px; 71 | text-align: center; 72 | font-style: italic; 73 | } 74 | `; 75 | } 76 | } 77 | 78 | export default BasePronoteCard; -------------------------------------------------------------------------------- /src/cards/base-period-related-card.js: -------------------------------------------------------------------------------- 1 | import BasePronoteCard from "./base-card"; 2 | 3 | const LitElement = Object.getPrototypeOf( 4 | customElements.get("ha-panel-lovelace") 5 | ); 6 | const html = LitElement.prototype.html; 7 | const css = LitElement.prototype.css; 8 | 9 | class BasePeriodRelatedPronoteCard extends BasePronoteCard { 10 | 11 | period_filter = null; 12 | allow_all_periods = true; 13 | items_attribute_key = null; 14 | period_sensor_key = null; 15 | 16 | getPeriodSwitcher() { 17 | if (this.period_filter === null) { 18 | this.setPeriodFilterFromConfig(this.config.default_period); 19 | } 20 | 21 | if (this.config.hide_period_switch) { 22 | return html``; 23 | } 24 | 25 | let available_periods = [...this.getActivePeriods()]; 26 | if (this.allow_all_periods) { 27 | available_periods.push({ 28 | id: 'all', 29 | name: 'Tout' 30 | }); 31 | } 32 | let tabs = []; 33 | for (let period of available_periods) { 34 | tabs.push( 35 | html` 43 | ` 44 | ); 45 | } 46 | 47 | return html`
${tabs}
`; 48 | } 49 | 50 | handlePeriodChange(event) { 51 | this.period_filter = event.target.value; 52 | this.requestUpdate(); 53 | } 54 | 55 | setPeriodFilterFromConfig() { 56 | if (this.config.default_period && this.period_filter === null) { 57 | if (this.config.default_period === 'current') { 58 | let active_periods = this.getActivePeriods(); 59 | for (let period of active_periods) { 60 | if (period.is_current_period) { 61 | this.period_filter = period.id; 62 | break; 63 | } 64 | } 65 | } 66 | else { 67 | this.period_filter = this.config.default_period; 68 | } 69 | } 70 | this.requestUpdate(); 71 | } 72 | 73 | getActivePeriodsSensor() { 74 | let sensor_prefix = this.config.entity.split('_'+this.period_sensor_key)[0]; 75 | return this.hass.states[`${sensor_prefix}_active_periods`]; 76 | } 77 | 78 | getActivePeriods() { 79 | return this.getActivePeriodsSensor().attributes['periods']; 80 | } 81 | 82 | getAllEntityNames() { 83 | let active_periods = this.getActivePeriods(); 84 | let entities = [ 85 | this.config.entity 86 | ]; 87 | for (let period of active_periods) { 88 | if (!period.is_current_period) { 89 | entities.push(`${this.config.entity}_${period.id}`); 90 | } 91 | } 92 | return entities; 93 | } 94 | 95 | getFilteredItems() { 96 | if (this.period_filter === 'all' && !this.allow_all_periods) { 97 | this.period_filter = this.getActivePeriods()[this.getActivePeriods().length - 1].id; 98 | } 99 | 100 | let entity_names = this.getAllEntityNames(); 101 | let items = []; 102 | for (let entity_name of entity_names) { 103 | let entity_state = this.hass.states[entity_name]; 104 | if (this.period_filter === 'all' || this.period_filter === entity_state.attributes['period_key']) { 105 | items.push(...entity_state.attributes[this.items_attribute_key]); 106 | } 107 | } 108 | return items; 109 | } 110 | 111 | getCurrentPeriodKey() { 112 | let active_periods = this.getActivePeriods(); 113 | for (let period of active_periods) { 114 | if (period.is_current_period) { 115 | return period.id; 116 | } 117 | } 118 | return null; 119 | } 120 | 121 | isCurrentPeriodSelected() { 122 | return this.period_filter === this.getCurrentPeriodKey(); 123 | } 124 | 125 | getDefaultConfig() { 126 | return { 127 | default_period: 'current', 128 | hide_period_switch: false, 129 | } 130 | } 131 | 132 | static get styles() { 133 | return css` 134 | ${super.styles} 135 | .pronote-period-switcher { 136 | display: flex; 137 | justify-content: center; 138 | gap: 10px; 139 | padding:5px; 140 | } 141 | .pronote-period-switcher input { 142 | display: none; 143 | } 144 | .pronote-period-switcher-tab { 145 | padding: 10px; 146 | } 147 | .pronote-period-switcher-tab:hover { 148 | cursor: pointer; 149 | } 150 | .pronote-period-switcher input:checked + .pronote-period-switcher-tab { 151 | background-color: rgba(0,0,0,0.1); 152 | } 153 | `; 154 | } 155 | } 156 | 157 | export default BasePeriodRelatedPronoteCard; -------------------------------------------------------------------------------- /src/cards/delays-card.js: -------------------------------------------------------------------------------- 1 | import BasePeriodRelatedPronoteCard from './base-period-related-card'; 2 | 3 | const LitElement = Object.getPrototypeOf( 4 | customElements.get("ha-panel-lovelace") 5 | ); 6 | const html = LitElement.prototype.html; 7 | const css = LitElement.prototype.css; 8 | 9 | class PronoteDelaysCard extends BasePeriodRelatedPronoteCard { 10 | 11 | header_title = 'Retards de ' 12 | no_data_message = 'Aucun retard' 13 | period_sensor_key = 'delays' 14 | items_attribute_key = 'delays' 15 | 16 | // Génère une ligne pour un retard donné 17 | getDelaysRow(delay) { 18 | const date = this.getFormattedDate(delay.date); 19 | 20 | const rowContent = html` 21 | 22 | 23 | 24 | ${delay.justified 25 | ? html`` 26 | : html``} 27 | 28 | 29 | 30 | 31 | 32 | 33 | ${date} 34 |
35 | ${delay.minutes} minutes de retard 36 | 37 | 38 | ${delay.reasons} 39 | 40 | 41 | `; 42 | 43 | return html`${rowContent}`; 44 | } 45 | 46 | // Formate la date d'un retard 47 | getFormattedDate(date) { 48 | return (new Date(date)) 49 | ? new Date(date).toLocaleDateString('fr-FR', { 50 | weekday: 'long', 51 | day: '2-digit', 52 | month: '2-digit', 53 | hour: '2-digit', 54 | minute: '2-digit' 55 | }).replace(/^(.)/, (match) => match.toUpperCase()) 56 | : ''; 57 | } 58 | 59 | // Génère le rendu de la carte 60 | getCardContent() { 61 | const stateObj = this.hass.states[this.config.entity]; 62 | 63 | if (stateObj) { 64 | 65 | const delays = this.getFilteredItems(); 66 | const itemTemplates = [ 67 | this.getPeriodSwitcher() 68 | ]; 69 | let dayTemplates = []; 70 | let delaysCount = 0; 71 | 72 | // Génère les lignes pour chaque retard, en respectant la limite maximale si définie 73 | for (let index = 0; index < delays.length; index++) { 74 | delaysCount++; 75 | 76 | if (this.config.max_delays && this.config.max_delays < delaysCount) { 77 | break; 78 | } 79 | 80 | const currentDelay = delays[index]; 81 | dayTemplates.push(this.getDelaysRow(currentDelay)); 82 | } 83 | 84 | if (dayTemplates.length > 0) { 85 | itemTemplates.push(html`${dayTemplates}
`); 86 | } else { 87 | itemTemplates.push(this.noDataMessage()); 88 | } 89 | 90 | return itemTemplates; 91 | } 92 | } 93 | 94 | getDefaultConfig() { 95 | return { 96 | ...super.getDefaultConfig(), 97 | display_header: true, 98 | max_delays: null, 99 | } 100 | } 101 | 102 | static get styles() { 103 | return css` 104 | ${super.styles} 105 | table{ 106 | clear:both; 107 | font-size: 0.9em; 108 | font-family: Roboto; 109 | width: 100%; 110 | outline: 0px solid #393c3d; 111 | border-collapse: collapse; 112 | } 113 | tr:nth-child(odd) { 114 | background-color: rgba(0,0,0,0.1); 115 | } 116 | td { 117 | vertical-align: middle; 118 | padding: 5px 10px 5px 10px; 119 | text-align: left; 120 | } 121 | tr td:first-child { 122 | width: 13%; 123 | text-align:right; 124 | } 125 | span.delay-reason { 126 | font-weight:bold; 127 | display:block; 128 | } 129 | tr td:nth-child(2) { 130 | width: 4px; 131 | padding: 5px 0; 132 | } 133 | tr td:nth-child(2) > span { 134 | display:inline-block; 135 | width: 4px; 136 | height: 3rem; 137 | border-radius:4px; 138 | background-color: grey; 139 | margin-top:4px; 140 | } 141 | span.delay-from { 142 | color: white; 143 | font-weight:bold; 144 | padding: 4px; 145 | border-radius: 4px; 146 | } 147 | span.delay-hours { 148 | font-size: 0.9em; 149 | padding: 4px; 150 | } 151 | table + div { 152 | border-top: 1px solid white; 153 | } 154 | `; 155 | } 156 | 157 | static getStubConfig() { 158 | return { 159 | display_header: true, 160 | max_delays: null, 161 | } 162 | } 163 | 164 | static getConfigElement() { 165 | return document.createElement("pronote-delays-card-editor"); 166 | } 167 | } 168 | 169 | customElements.define("pronote-delays-card", PronoteDelaysCard); 170 | 171 | window.customCards = window.customCards || []; 172 | window.customCards.push({ 173 | type: "pronote-delays-card", 174 | name: "Pronote Delays Card", 175 | description: "Display the delays from Pronote", 176 | documentationURL: "https://github.com/delphiki/lovelace-pronote?tab=readme-ov-file#delays", 177 | }); -------------------------------------------------------------------------------- /src/cards/evaluations-card.js: -------------------------------------------------------------------------------- 1 | import BasePeriodRelatedPronoteCard from "./base-period-related-card"; 2 | 3 | const LitElement = Object.getPrototypeOf( 4 | customElements.get("ha-panel-lovelace") 5 | ); 6 | const html = LitElement.prototype.html; 7 | const css = LitElement.prototype.css; 8 | 9 | class PronoteEvaluationsCard extends BasePeriodRelatedPronoteCard { 10 | 11 | header_title = 'Evaluations de ' 12 | no_data_message = 'Pas d\'évaluation à afficher' 13 | period_sensor_key = 'evaluations' 14 | items_attribute_key = 'evaluations' 15 | 16 | getFormattedDate(date) { 17 | return (new Date(date)) 18 | .toLocaleDateString('fr-FR', {weekday: 'short', day: '2-digit', month: '2-digit'}) 19 | .replace(/^(.)/, (match) => match.toUpperCase()) 20 | ; 21 | } 22 | 23 | getAcquisitionRow(acquisition) { 24 | return html` 25 | ${acquisition.name} 26 | ${this.getAcquisitionIcon(acquisition)} 27 | `; 28 | } 29 | 30 | getAcquisitionIcon(acquisition) { 31 | const remappedEvaluations = this.config.mapping_evaluations[acquisition.abbreviation] || acquisition.abbreviation; 32 | let icon = ''; 33 | if (remappedEvaluations === 'A+') { 34 | icon = '+'; 35 | } else if (remappedEvaluations === 'Abs') { 36 | icon = 'a'; 37 | } 38 | return html` 39 | 40 | ${icon} 41 | 42 | `; 43 | } 44 | 45 | getEvaluationRow(evaluation, index, lessons_colors) { 46 | let acquisitions = evaluation.acquisitions; 47 | let acquisitionIcons = []; 48 | let acquisitionsRows = []; 49 | let lesson_background_color = lessons_colors[evaluation.subject]; 50 | 51 | for (let i = 0; i < acquisitions.length; i++) { 52 | acquisitionIcons.push(this.getAcquisitionIcon(acquisitions[i])); 53 | acquisitionsRows.push(this.getAcquisitionRow(acquisitions[i])); 54 | } 55 | 56 | return html` 57 | 58 | 59 | 60 | 63 | ${this.config.display_teacher ? html`${evaluation.teacher}` : ''} 64 | 65 | ${this.config.display_comment ? html`${evaluation.name}` : ''} 66 | ${this.config.display_description ? html`${evaluation.description}` : ''} 67 | ${this.config.display_date ? html`${this.getFormattedDate(evaluation.date)}`: ''} 68 | ${this.config.display_coefficient && evaluation.coefficient ? html`Coef. ${evaluation.coefficient}` : ''} 69 | 70 | 71 | ${acquisitionIcons} 72 | 73 | 74 | ${acquisitionsRows} 75 | `; 76 | } 77 | 78 | getCardContent() { 79 | const stateObj = this.hass.states[this.config.entity]; 80 | 81 | if (stateObj) { 82 | const evaluations = this.getFilteredItems(); 83 | const max_evaluations = this.config.max_evaluations ?? evaluations.length; 84 | 85 | const evaluationsRows = []; 86 | const itemTemplates = [ 87 | this.getPeriodSwitcher() 88 | ]; 89 | 90 | // Va chercher les couleurs des matières 91 | const lessons = this.hass.states[this.config.entity.replace("_evaluations", "_period_s_timetable")].attributes['lessons'] 92 | var lessons_colors = {}; 93 | if (lessons) { 94 | for (let index = 0; index < lessons.length; index++) { 95 | let lesson = lessons[index]; 96 | lessons_colors[lesson.lesson]=lesson.background_color; 97 | } 98 | } 99 | 100 | for (let index = 0; index < max_evaluations; index++) { 101 | let evaluation = evaluations[index]; 102 | evaluationsRows.push(this.getEvaluationRow(evaluation, index, lessons_colors)); 103 | } 104 | 105 | if (evaluationsRows.length > 0) { 106 | itemTemplates.push(html`${evaluationsRows}
`); 107 | } else { 108 | itemTemplates.push(this.noDataMessage()); 109 | } 110 | 111 | return itemTemplates; 112 | } 113 | 114 | return []; 115 | } 116 | 117 | getDefaultConfig() { 118 | return { 119 | ...super.getDefaultConfig(), 120 | display_header: true, 121 | display_description: true, 122 | display_teacher: true, 123 | display_date: true, 124 | display_comment: true, 125 | display_coefficient: true, 126 | max_evaluations: null, 127 | mapping_evaluations: {}, 128 | } 129 | } 130 | 131 | static get styles() { 132 | return css` 133 | ${super.styles} 134 | table { 135 | font-size: 0.9em; 136 | font-family: Roboto; 137 | width: 100%; 138 | outline: 0px solid #393c3d; 139 | border-collapse: collapse; 140 | } 141 | td { 142 | vertical-align: top; 143 | padding: 5px 10px 5px 10px; 144 | text-align: left; 145 | } 146 | td.evaluation-color { 147 | width: 4px; 148 | padding-top: 11px; 149 | } 150 | td.evaluation-color > span { 151 | display:inline-block; 152 | width: 4px; 153 | height: 2rem; 154 | border-radius:4px; 155 | background-color: grey; 156 | } 157 | .above-average .evaluation-color > span, .above-ratio .evaluation-color > span { 158 | background-color: green; 159 | } 160 | .below-average .evaluation-color > span, .below-ratio .evaluation-color > span { 161 | background-color: orange; 162 | } 163 | .evaluation-subject { 164 | font-weight: bold; 165 | display: block; 166 | } 167 | .evaluation-description { 168 | display: block; 169 | } 170 | .evaluation-teacher { 171 | display: block; 172 | } 173 | .evaluation-date, .evaluation-coefficient { 174 | font-size: 0.9em; 175 | color: gray; 176 | } 177 | .evaluation-comment { 178 | display: block; 179 | } 180 | .evaluation-date + .evaluation-coefficient:before { 181 | content: ' - ' 182 | } 183 | .evaluation-detail { 184 | text-align: right; 185 | } 186 | .evaluation-value { 187 | font-weight: bold; 188 | } 189 | .acquisition-icon { 190 | display: inline-block; 191 | width:14px; 192 | height:14px; 193 | border-radius:50%; 194 | border: solid 0.02em rgba(0, 0, 0, 0.5); 195 | margin-left: 4px; 196 | vertical-align: middle; 197 | color: white; 198 | content: '+'; 199 | text-align:center; 200 | line-height:14px; 201 | } 202 | .acquisition-icon-Aplus { 203 | background-color: #008000; 204 | } 205 | .acquisition-icon-A { 206 | background-color: #45B851; 207 | } 208 | .acquisition-icon-B { 209 | background-color: ; 210 | } 211 | .acquisition-icon-C { 212 | background-color: #FFDA01; 213 | } 214 | .acquisition-icon-D { 215 | background-color: #F80A0A; 216 | } 217 | .acquisition-icon-E { 218 | background-color: #F80A0A; 219 | } 220 | .acquisition-row { 221 | display: none; 222 | } 223 | input[type="checkbox"] { 224 | display: none; 225 | } 226 | /** FIXME 227 | .evaluation-row:has(input:checked) .acquisition-icon { 228 | display:none; 229 | } 230 | .evaluation-row:has(input:checked) + .acquisition-row { 231 | display: table-row; 232 | } 233 | */ 234 | .acquisition-row td:nth-child(2) { 235 | text-align: right; 236 | } 237 | `; 238 | } 239 | 240 | static getStubConfig() { 241 | return { 242 | display_header: true, 243 | display_description: true, 244 | display_teacher: true, 245 | display_date: true, 246 | display_comment: true, 247 | display_coefficient: true, 248 | max_evaluations: null, 249 | mapping_evaluations: {}, 250 | } 251 | } 252 | 253 | static getConfigElement() { 254 | return document.createElement("pronote-evaluations-card-editor"); 255 | } 256 | } 257 | 258 | customElements.define("pronote-evaluations-card", PronoteEvaluationsCard); 259 | 260 | window.customCards = window.customCards || []; 261 | window.customCards.push({ 262 | type: "pronote-evaluations-card", 263 | name: "Pronote Evaluations Card", 264 | description: "Display the evaluations from Pronote", 265 | documentationURL: "https://github.com/delphiki/lovelace-pronote?tab=readme-ov-file#evaluations", 266 | }); -------------------------------------------------------------------------------- /src/cards/grades-card.js: -------------------------------------------------------------------------------- 1 | import BasePeriodRelatedPronoteCard from './base-period-related-card'; 2 | 3 | const LitElement = Object.getPrototypeOf( 4 | customElements.get("ha-panel-lovelace") 5 | ); 6 | 7 | const html = LitElement.prototype.html; 8 | const css = LitElement.prototype.css; 9 | 10 | class PronoteGradesCard extends BasePeriodRelatedPronoteCard { 11 | 12 | header_title = 'Notes de ' 13 | no_data_message = 'Aucune note disponible' 14 | period_sensor_key = 'grades' 15 | items_attribute_key = 'grades' 16 | 17 | getFormattedDate(date) { 18 | return (new Date(date)) 19 | .toLocaleDateString('fr-FR', {weekday: 'short', day: '2-digit', month: '2-digit'}) 20 | .replace(/^(.)/, (match) => match.toUpperCase()) 21 | ; 22 | } 23 | 24 | getGradeRow(gradeData) { 25 | let grade = parseFloat(gradeData.grade.replace(',', '.')); 26 | 27 | let grade_classes = []; 28 | 29 | if (this.config.compare_with_ratio !== null) { 30 | let comparison_ratio = parseFloat(this.config.compare_with_ratio); 31 | let grade_ratio = grade / parseFloat(gradeData.out_of.replace(',', '.')); 32 | grade_classes.push(grade_ratio >= comparison_ratio ? 'above-ratio' : 'below-ratio'); 33 | } else if (this.config.compare_with_class_average && gradeData.class_average) { 34 | let class_average = parseFloat(gradeData.class_average.replace(',', '.')); 35 | grade_classes.push(grade > class_average ? 'above-average' : 'below-average'); 36 | } 37 | 38 | let formatted_grade = gradeData.grade_out_of; 39 | if (this.config.grade_format === 'short') { 40 | formatted_grade = gradeData.grade; 41 | } 42 | 43 | if (this.config.display_new_grade_notice) { 44 | let grade_date = new Date(gradeData.date); 45 | let today = new Date(); 46 | if ( 47 | grade_date.getFullYear() === today.getFullYear() 48 | && grade_date.getMonth() === today.getMonth() 49 | && grade_date.getDate() === today.getDate() 50 | ) { 51 | grade_classes.push('new-grade'); 52 | } 53 | } 54 | 55 | return html` 56 | 57 | 58 | 59 | ${gradeData.subject} 60 | ${this.config.display_comment ? html`${gradeData.comment}` : ''} 61 | ${this.config.display_date ? html`${this.getFormattedDate(gradeData.date)}`: ''} 62 | ${this.config.display_coefficient && gradeData.coefficient ? html`Coef. ${gradeData.coefficient}` : ''} 63 | 64 | 65 | ${formatted_grade} 66 | ${this.config.display_class_average && gradeData.class_average ? html`Moy. ${gradeData.class_average}` : ''} 67 | ${this.config.display_class_min && gradeData.min ? html`Min. ${gradeData.min}` : ''} 68 | ${this.config.display_class_max && gradeData.max ? html`Max. ${gradeData.max}` : ''} 69 | 70 | 71 | `; 72 | } 73 | 74 | getCardContent() { 75 | const stateObj = this.hass.states[this.config.entity]; 76 | const grades = this.getFilteredItems(); 77 | const max_grades = this.config.max_grades ?? grades.length; 78 | 79 | if (stateObj) { 80 | 81 | const gradesRows = []; 82 | const itemTemplates = [ 83 | this.getPeriodSwitcher() 84 | ]; 85 | 86 | 87 | for (let index = 0; index < max_grades; index++) { 88 | let grade = grades[index]; 89 | gradesRows.push(this.getGradeRow(grade)); 90 | } 91 | 92 | if (gradesRows.length > 0) { 93 | itemTemplates.push(html`${gradesRows}
`); 94 | } else { 95 | itemTemplates.push(this.noDataMessage()); 96 | } 97 | 98 | return itemTemplates; 99 | } 100 | } 101 | 102 | getDefaultConfig() { 103 | return { 104 | ...super.getDefaultConfig(), 105 | grade_format: 'full', 106 | display_header: true, 107 | display_date: true, 108 | display_comment: true, 109 | display_class_average: true, 110 | compare_with_class_average: true, 111 | compare_with_ratio: null, 112 | display_coefficient: true, 113 | display_class_min: true, 114 | display_class_max: true, 115 | display_new_grade_notice: true, 116 | max_grades: null, 117 | }; 118 | } 119 | 120 | static get styles() { 121 | return css` 122 | ${super.styles} 123 | table { 124 | font-size: 0.9em; 125 | font-family: Roboto; 126 | width: 100%; 127 | outline: 0px solid #393c3d; 128 | border-collapse: collapse; 129 | } 130 | td { 131 | vertical-align: top; 132 | padding: 5px 10px 5px 10px; 133 | padding-top: 8px; 134 | text-align: left; 135 | } 136 | td.grade-color { 137 | width: 4px; 138 | padding-top: 11px; 139 | } 140 | td.grade-color > span { 141 | display:inline-block; 142 | width: 4px; 143 | height: 1rem; 144 | border-radius:4px; 145 | background-color: grey; 146 | } 147 | .above-average .grade-color > span, .above-ratio .grade-color > span { 148 | background-color: green; 149 | } 150 | .below-average .grade-color > span, .below-ratio .grade-color > span { 151 | background-color: orange; 152 | } 153 | .grade-description { 154 | padding-left: 0; 155 | } 156 | .grade-subject { 157 | display: inline-block; 158 | font-weight: bold; 159 | position: relative; 160 | } 161 | .new-grade .grade-subject:after { 162 | content: " "; 163 | display: block; 164 | width: 6px; 165 | height: 6px; 166 | border-radius: 6px; 167 | background-color: orange; 168 | position: absolute; 169 | top: 6px; 170 | right: -14px; 171 | } 172 | .grade-comment { 173 | display: block; 174 | } 175 | .grade-date, .grade-coefficient { 176 | font-size: 0.9em; 177 | color: gray; 178 | } 179 | .grade-date + .grade-coefficient:before { 180 | content: ' - ' 181 | } 182 | .grade-detail { 183 | text-align: right; 184 | } 185 | .grade-value { 186 | font-weight: bold; 187 | } 188 | .grade-value, .grade-class-average { 189 | display:block; 190 | } 191 | .grade-class-average, .grade-class-min, .grade-class-max { 192 | font-size: 0.9em; 193 | color: gray; 194 | } 195 | .grade-class-min + .grade-class-max:before { 196 | content: ' - ' 197 | } 198 | `; 199 | } 200 | 201 | static getStubConfig() { 202 | return { 203 | grade_format: 'full', 204 | display_header: true, 205 | display_date: true, 206 | display_comment: true, 207 | display_class_average: true, 208 | compare_with_class_average: true, 209 | compare_with_ratio: null, 210 | display_coefficient: true, 211 | display_class_min: true, 212 | display_class_max: true, 213 | display_new_grade_notice: true, 214 | max_grades: null, 215 | } 216 | } 217 | 218 | static getConfigElement() { 219 | return document.createElement("pronote-grades-card-editor"); 220 | } 221 | } 222 | 223 | customElements.define("pronote-grades-card", PronoteGradesCard); 224 | 225 | window.customCards = window.customCards || []; 226 | window.customCards.push({ 227 | type: "pronote-grades-card", 228 | name: "Pronote Grades Card", 229 | description: "Display the grades from Pronote", 230 | documentationURL: "https://github.com/delphiki/lovelace-pronote?tab=readme-ov-file#grades", 231 | }); -------------------------------------------------------------------------------- /src/cards/homework-card.js: -------------------------------------------------------------------------------- 1 | import BasePronoteCard from "./base-card" 2 | import {unsafeHTML} from 'lit-html/directives/unsafe-html.js'; 3 | 4 | const LitElement = Object.getPrototypeOf( 5 | customElements.get("ha-panel-lovelace") 6 | ); 7 | const html = LitElement.prototype.html; 8 | const css = LitElement.prototype.css; 9 | 10 | Date.prototype.getWeekNumber = function () { 11 | var d = new Date(+this); 12 | d.setHours(0, 0, 0, 0); 13 | d.setDate(d.getDate() + 4 - (d.getDay() || 7)); 14 | return Math.ceil((((d - new Date(d.getFullYear(), 0, 1)) / 8.64e7) + 1) / 7); 15 | }; 16 | 17 | class PronoteHomeworkCard extends BasePronoteCard { 18 | 19 | lunchBreakRendered = false; 20 | 21 | getFormattedDate(date) { 22 | return (new Date(date)) 23 | .toLocaleDateString('fr-FR', {weekday: 'long', day: '2-digit', month: '2-digit'}) 24 | .replace(/^(.)/, (match) => match.toUpperCase()) 25 | ; 26 | } 27 | 28 | getFormattedTime(time) { 29 | return new Intl.DateTimeFormat("fr-FR", {hour:"numeric", minute:"numeric"}).format(new Date(time)); 30 | } 31 | 32 | getDayHeader(homework) { 33 | return html`
34 | ${this.getFormattedDate(homework.date)} 35 |
`; 36 | } 37 | 38 | getHomeworkRow(homework, index) { 39 | let description = homework.description.trim().replace("\n", "
"); 40 | let files = []; 41 | homework.files.forEach((file) => { 42 | if (file.name.trim() === '') { 43 | return; 44 | } 45 | files.push(html`${file.name}`); 46 | }); 47 | 48 | 49 | return html` 50 | 51 | 52 | 53 | 56 | 57 | ${unsafeHTML(description)} 58 | ${files.length > 0 ? html`${files}` : ''} 59 | 60 | 61 | ${homework.done ? html`` : html``} 62 | 63 | 64 | `; 65 | } 66 | 67 | getCardContent() { 68 | 69 | const stateObj = this.hass.states[this.config.entity]; 70 | const homework = this.hass.states[this.config.entity].attributes['homework']; 71 | 72 | if (stateObj) { 73 | const currentWeekNumber = new Date().getWeekNumber(); 74 | const itemTemplates = []; 75 | let dayTemplates = []; 76 | 77 | if (homework && homework.length > 0) { 78 | let latestHomeworkDay = this.getFormattedDate(homework[0].date); 79 | for (let index = 0; index < homework.length; index++) { 80 | let hw = homework[index]; 81 | let currentFormattedDate = this.getFormattedDate(hw.date); 82 | 83 | if (hw.done === true && this.config.display_done_homework === false) { 84 | continue; 85 | } 86 | 87 | if (latestHomeworkDay !== currentFormattedDate) { 88 | if (dayTemplates.length > 0) { 89 | itemTemplates.push(this.getDayHeader(homework[index-1])); 90 | itemTemplates.push(html`${dayTemplates}
`); 91 | dayTemplates = []; 92 | } 93 | 94 | latestHomeworkDay = currentFormattedDate; 95 | } 96 | 97 | if (this.config.current_week_only && new Date(hw.date).getWeekNumber() !== currentWeekNumber) { 98 | break; 99 | } 100 | 101 | dayTemplates.push(this.getHomeworkRow(hw, index)); 102 | } 103 | 104 | if (dayTemplates.length > 0 && ( 105 | !this.config.current_week_only 106 | || (this.config.current_week_only && currentWeekNumber === new Date(homework[homework.length-1].date).getWeekNumber()) 107 | )) { 108 | itemTemplates.push(this.getDayHeader(homework[homework.length-1])); 109 | itemTemplates.push(html`${dayTemplates}
`); 110 | } 111 | } 112 | 113 | if (itemTemplates.length === 0) { 114 | itemTemplates.push(this.noDataMessage()); 115 | } 116 | 117 | return itemTemplates; 118 | } 119 | } 120 | 121 | setConfig(config) { 122 | if (!config.entity) { 123 | throw new Error('You need to define an entity'); 124 | } 125 | 126 | const defaultConfig = { 127 | entity: null, 128 | display_header: true, 129 | current_week_only: true, 130 | reduce_done_homework: true, 131 | display_done_homework: true, 132 | } 133 | 134 | this.config = { 135 | ...defaultConfig, 136 | ...config 137 | }; 138 | 139 | this.header_title = 'Devoirs de '; 140 | this.no_data_message = 'Pas de devoirs à faire'; 141 | } 142 | 143 | static get styles() { 144 | return css` 145 | ${super.styles} 146 | .pronote-homework-header { 147 | border-bottom: 2px solid grey; 148 | } 149 | table{ 150 | font-size: 0.9em; 151 | font-family: Roboto; 152 | width: 100%; 153 | outline: 0px solid #393c3d; 154 | border-collapse: collapse; 155 | } 156 | td { 157 | vertical-align: top; 158 | padding: 5px 10px 5px 10px; 159 | padding-top: 8px; 160 | text-align: left; 161 | } 162 | td.homework-color { 163 | width: 4px; 164 | padding-top: 11px; 165 | } 166 | td.homework-color > span { 167 | display:inline-block; 168 | width: 4px; 169 | height: 1rem; 170 | border-radius:4px; 171 | background-color: grey; 172 | } 173 | td.homework-detail { 174 | padding:0; 175 | padding-top: 8px; 176 | padding-bottom: 8px; 177 | } 178 | span.homework-subject { 179 | display:block; 180 | font-weight:bold; 181 | } 182 | span.homework-description { 183 | font-size: 0.9em; 184 | } 185 | span.homework-files { 186 | display: block; 187 | } 188 | span.homework-files .homework-file { 189 | display: inline-block; 190 | } 191 | td.homework-status { 192 | width: 5%; 193 | } 194 | .reduce-done .homework-done label:hover { 195 | cusor: pointer; 196 | } 197 | .reduce-done .homework-done .homework-description { 198 | display: none; 199 | } 200 | .reduce-done .homework-done input:checked + .homework-description { 201 | display: block; 202 | } 203 | .homework-detail input { 204 | display: none; 205 | } 206 | `; 207 | } 208 | 209 | static getStubConfig() { 210 | return { 211 | display_header: true, 212 | current_week_only: true, 213 | reduce_done_homework: true, 214 | display_done_homework: true, 215 | } 216 | } 217 | 218 | static getConfigElement() { 219 | return document.createElement("pronote-homework-card-editor"); 220 | } 221 | } 222 | 223 | customElements.define("pronote-homework-card", PronoteHomeworkCard); 224 | 225 | window.customCards = window.customCards || []; 226 | window.customCards.push({ 227 | type: "pronote-homework-card", 228 | name: "Pronote Homework Card", 229 | description: "Display the homework from Pronote", 230 | documentationURL: "https://github.com/delphiki/lovelace-pronote?tab=readme-ov-file#homework", 231 | }); 232 | -------------------------------------------------------------------------------- /src/cards/timetable-card.js: -------------------------------------------------------------------------------- 1 | import BasePronoteCard from "./base-card" 2 | 3 | const LitElement = Object.getPrototypeOf( 4 | customElements.get("ha-panel-lovelace") 5 | ); 6 | const html = LitElement.prototype.html; 7 | const css = LitElement.prototype.css; 8 | 9 | Date.prototype.getWeekNumber = function () { 10 | var d = new Date(+this); 11 | d.setHours(0, 0, 0, 0); 12 | d.setDate(d.getDate() + 4 - (d.getDay() || 7)); 13 | return Math.ceil((((d - new Date(d.getFullYear(), 0, 1)) / 8.64e7) + 1) / 7); 14 | }; 15 | 16 | class PronoteTimetableCard extends BasePronoteCard { 17 | 18 | lunchBreakRendered = false; 19 | 20 | getBreakRow(label, ended) { 21 | return html` 22 | 23 | 24 | 25 | 26 | ${label} 27 | 28 | `; 29 | } 30 | 31 | getTimetableRow(lesson) { 32 | let currentDate = new Date(); 33 | let startAt = Date.parse(lesson.start_at); 34 | let endAt = Date.parse(lesson.end_at); 35 | 36 | let prefix = html``; 37 | if (this.config.display_lunch_break && lesson.is_afternoon && !this.lunchBreakRendered) { 38 | prefix = this.getBreakRow('Repas', this.config.dim_ended_lessons && startAt < currentDate); 39 | this.lunchBreakRendered = true; 40 | } 41 | 42 | let content = html` 43 | 44 | 45 | ${lesson.start_time}
46 | ${lesson.end_time} 47 | 48 | 49 | 50 | ${lesson.lesson} 51 | ${this.config.display_classroom ? html` 52 | ${lesson.classroom ? 'Salle '+lesson.classroom : ''} 53 | ${lesson.classroom && this.config.display_teacher ? ', ' : '' } 54 | ` : '' } 55 | ${this.config.display_teacher ? html` 56 | ${lesson.teacher_name} 57 | `: '' } 58 | 59 | 60 | ${lesson.status ? html`${lesson.status}`:''} 61 | 62 | 63 | ` 64 | return html`${prefix}${content}`; 65 | } 66 | 67 | getFormattedDate(lesson) { 68 | return (new Date(lesson.start_at)) 69 | .toLocaleDateString('fr-FR', {weekday: 'long', day: '2-digit', month: '2-digit'}) 70 | .replace(/^(.)/, (match) => match.toUpperCase()) 71 | ; 72 | } 73 | 74 | getFormattedTime(time) { 75 | return new Intl.DateTimeFormat("fr-FR", {hour:"numeric", minute:"numeric"}).format(new Date(time)); 76 | } 77 | 78 | getDayHeader(firstLesson, dayStartAt, dayEndAt, daysCount) { 79 | return html`
80 | ${this.config.enable_slider ? html` this.changeDay('previous', e)} 83 | >←` : '' } 84 | ${this.getFormattedDate(firstLesson)} 85 | ${this.config.display_day_hours && dayStartAt && dayEndAt ? html` 86 | ${this.getFormattedTime(dayStartAt)} - ${this.getFormattedTime(dayEndAt)} 87 | ` : '' } 88 | ${this.config.enable_slider ? html` this.changeDay('next', e)} 91 | >→` : '' } 92 |
`; 93 | } 94 | 95 | changeDay(direction, e) { 96 | e.preventDefault(); 97 | if (e.target.classList.contains('disabled')) { 98 | return; 99 | } 100 | 101 | const activeDay = e.target.parentElement.parentElement; 102 | let hasPreviousDay = activeDay.previousElementSibling && activeDay.previousElementSibling.classList.contains('pronote-timetable-day-wrapper'); 103 | let hasNextDay = activeDay.nextElementSibling && activeDay.nextElementSibling.classList.contains('pronote-timetable-day-wrapper'); 104 | let newActiveDay = null; 105 | 106 | if (direction === 'previous' && hasPreviousDay) { 107 | newActiveDay = activeDay.previousElementSibling; 108 | } else if (direction === 'next' && hasNextDay) { 109 | newActiveDay = activeDay.nextElementSibling; 110 | } 111 | 112 | if (newActiveDay) { 113 | activeDay.classList.remove('active'); 114 | newActiveDay.classList.add('active'); 115 | 116 | hasPreviousDay = newActiveDay.previousElementSibling && newActiveDay.previousElementSibling.classList.contains('pronote-timetable-day-wrapper'); 117 | hasNextDay = newActiveDay.nextElementSibling && newActiveDay.nextElementSibling.classList.contains('pronote-timetable-day-wrapper'); 118 | 119 | if (!hasPreviousDay) { 120 | newActiveDay.querySelector('.pronote-timetable-header-arrow-left').classList.add('disabled'); 121 | } 122 | 123 | if (!hasNextDay) { 124 | newActiveDay.querySelector('.pronote-timetable-header-arrow-right').classList.add('disabled'); 125 | } 126 | } 127 | } 128 | 129 | // we override the render method to return the card content 130 | render() { 131 | if (!this.config || !this.hass) { 132 | return html``; 133 | } 134 | 135 | const stateObj = this.hass.states[this.config.entity]; 136 | 137 | const lessons = this.hass.states[this.config.entity].attributes['lessons'] 138 | 139 | if (stateObj) { 140 | this.lunchBreakRendered = false; 141 | const currentWeekNumber = new Date().getWeekNumber(); 142 | 143 | const itemTemplates = []; 144 | let dayTemplates = []; 145 | let daysCount = 0; 146 | 147 | let dayStartAt = null; 148 | let dayEndAt = null; 149 | 150 | for (let index = 0; index < lessons.length; index++) { 151 | let lesson = lessons[index]; 152 | let currentFormattedDate = this.getFormattedDate(lesson); 153 | 154 | if (!lesson.canceled) { 155 | if (dayStartAt === null) { 156 | dayStartAt = lesson.start_at; 157 | } 158 | dayEndAt = lesson.end_at; 159 | } 160 | 161 | if (lesson.canceled && index < lessons.length - 1) { 162 | let nextLesson = lessons[index + 1]; 163 | if (lesson.start_at === nextLesson.start_at && !nextLesson.canceled) { 164 | continue; 165 | } 166 | } 167 | 168 | if (this.config.current_week_only) { 169 | if (new Date(lesson.start_at).getWeekNumber() > currentWeekNumber) { 170 | break; 171 | } 172 | } 173 | 174 | dayTemplates.push(this.getTimetableRow(lesson)); 175 | 176 | // checking if next lesson is on another day 177 | if (index + 1 >= lessons.length || ((index + 1) < lessons.length && currentFormattedDate !== this.getFormattedDate(lessons[index+1]))) { 178 | itemTemplates.push(html` 179 |
180 | ${this.getDayHeader(lesson, dayStartAt, dayEndAt, daysCount)} 181 | ${dayTemplates}
182 |
183 | `); 184 | dayTemplates = []; 185 | 186 | this.lunchBreakRendered = false; 187 | dayStartAt = null; 188 | dayEndAt = null; 189 | 190 | daysCount++; 191 | if (this.config.max_days && this.config.max_days <= daysCount) { 192 | break; 193 | } 194 | } else if (this.config.display_free_time_slots && index + 1 < lessons.length) { 195 | const currentEndAt = new Date(lesson.end_at); 196 | const nextLesson = lessons[index+1]; 197 | const nextLessonStartAt = new Date(nextLesson.start_at); 198 | if (lesson.is_morning === nextLesson.is_morning && Math.floor((nextLessonStartAt-currentEndAt) / 1000 / 60) > 30) { 199 | const now = new Date(); 200 | dayTemplates.push(this.getBreakRow('Pas de cours', this.config.dim_ended_lessons && nextLessonStartAt < now)); 201 | } 202 | } 203 | } 204 | 205 | if (dayTemplates.length > 0) { 206 | itemTemplates.push(html`${dayTemplates}
`); 207 | } 208 | 209 | return html` 210 | 211 | ${this.config.display_header ? this.getCardHeader() : ''} 212 | ${itemTemplates} 213 | ` 214 | ; 215 | } 216 | } 217 | 218 | setConfig(config) { 219 | if (!config.entity) { 220 | throw new Error('You need to define an entity'); 221 | } 222 | 223 | const defaultConfig = { 224 | entity: null, 225 | display_header: true, 226 | display_lunch_break: true, 227 | display_classroom: true, 228 | display_teacher: true, 229 | display_day_hours: true, 230 | dim_ended_lessons: true, 231 | max_days: null, 232 | current_week_only: false, 233 | enable_slider: false, 234 | display_free_time_slots: true, 235 | } 236 | 237 | this.config = { 238 | ...defaultConfig, 239 | ...config 240 | }; 241 | 242 | this.header_title = 'Emploi du temps de '; 243 | this.no_data_message = 'Pas d\'emploi du temps à afficher'; 244 | } 245 | 246 | static get styles() { 247 | return css` 248 | ${super.styles} 249 | .pronote-timetable-card-slider .pronote-timetable-day-wrapper { 250 | display: none; 251 | } 252 | .pronote-timetable-card-slider .pronote-timetable-day-wrapper.active { 253 | display: block; 254 | } 255 | .pronote-timetable-card-slider .pronote-timetable-header-date { 256 | display: inline-block; 257 | text-align: center; 258 | width: 120px; 259 | } 260 | .pronote-timetable-header-arrow-left, 261 | .pronote-timetable-header-arrow-right { 262 | cursor: pointer; 263 | } 264 | .pronote-timetable-header-arrow-left.disabled, 265 | .pronote-timetable-header-arrow-right.disabled { 266 | opacity: 0.3; 267 | pointer-events: none; 268 | } 269 | span.pronote-timetable-header-hours { 270 | float:right; 271 | } 272 | table{ 273 | clear:both; 274 | font-size: 0.9em; 275 | font-family: Roboto; 276 | width: 100%; 277 | outline: 0px solid #393c3d; 278 | border-collapse: collapse; 279 | } 280 | tr:nth-child(odd) { 281 | background-color: rgba(0,0,0,0.1); 282 | } 283 | td { 284 | vertical-align: middle; 285 | padding: 5px 10px 5px 10px; 286 | text-align: left; 287 | } 288 | tr td:first-child { 289 | width: 13%; 290 | text-align:right; 291 | } 292 | span.lesson-name { 293 | font-weight:bold; 294 | display:block; 295 | } 296 | tr td:nth-child(2) { 297 | width: 4px; 298 | padding: 5px 0; 299 | } 300 | tr td:nth-child(2) > span { 301 | display:inline-block; 302 | width: 4px; 303 | height: 3rem; 304 | border-radius:4px; 305 | background-color: grey; 306 | margin-top:4px; 307 | } 308 | span.lesson-status { 309 | color: white; 310 | background-color: rgb(75, 197, 253); 311 | padding: 4px; 312 | border-radius: 4px; 313 | } 314 | .lesson-canceled span.lesson-name { 315 | text-decoration: line-through; 316 | } 317 | .lesson-canceled span.lesson-status { 318 | background-color: rgb(250, 50, 75); 319 | } 320 | .lesson-ended { 321 | opacity: 0.3; 322 | } 323 | div:not(.slider-enabled).pronote-timetable-day-wrapper + div:not(.slider-enabled).pronote-timetable-day-wrapper { 324 | border-top: 1px solid white; 325 | } 326 | `; 327 | } 328 | 329 | static getStubConfig() { 330 | return { 331 | display_header: true, 332 | display_lunch_break: true, 333 | display_classroom: true, 334 | display_teacher: true, 335 | display_day_hours: true, 336 | dim_ended_lessons: true, 337 | max_days: null, 338 | current_week_only: false, 339 | enable_slider: false, 340 | display_free_time_slots: true, 341 | } 342 | } 343 | 344 | static getConfigElement() { 345 | return document.createElement("pronote-timetable-card-editor"); 346 | } 347 | } 348 | 349 | customElements.define("pronote-timetable-card", PronoteTimetableCard); 350 | 351 | window.customCards = window.customCards || []; 352 | window.customCards.push({ 353 | type: "pronote-timetable-card", 354 | name: "Pronote Timetable Card", 355 | description: "Display the timetable from Pronote", 356 | documentationURL: "https://github.com/delphiki/lovelace-pronote?tab=readme-ov-file#timetable", 357 | }); -------------------------------------------------------------------------------- /src/editors/absences-card-editor.js: -------------------------------------------------------------------------------- 1 | import BasePronoteCardEditor from "./base-editor"; 2 | 3 | const LitElement = Object.getPrototypeOf( 4 | customElements.get("ha-panel-lovelace") 5 | ); 6 | 7 | const html = LitElement.prototype.html; 8 | 9 | class PronoteAbsencesCardEditor extends BasePronoteCardEditor { 10 | render() { 11 | if (!this.hass || !this._config) { 12 | return html``; 13 | } 14 | 15 | return html` 16 | ${this.buildEntityPickerField('Absences entity', 'entity', this._config.entity, 'absences')} 17 | ${this.buildSwitchField('Display header', 'display_header', this._config.display_header)} 18 | ${this.buildNumberField('Max absences', 'max_absences', this._config.max_absences)} 19 | ${this.buildSwitchField('Hide period switch', 'hide_period_switch', this._config.hide_period_switch, false)} 20 | ${this.buildDefaultPeriodSelectField('Default period', 'default_period', 'absences', this._config.default_period)} 21 | `; 22 | } 23 | } 24 | 25 | customElements.define("pronote-absences-card-editor", PronoteAbsencesCardEditor); 26 | -------------------------------------------------------------------------------- /src/editors/averages-card-editor.js: -------------------------------------------------------------------------------- 1 | import BasePronoteCardEditor from "./base-editor"; 2 | 3 | const LitElement = Object.getPrototypeOf( 4 | customElements.get("ha-panel-lovelace") 5 | ); 6 | 7 | const html = LitElement.prototype.html; 8 | 9 | class PronoteAveragesCardEditor extends BasePronoteCardEditor { 10 | render() { 11 | if (!this.hass || !this._config) { 12 | return html``; 13 | } 14 | 15 | return html` 16 | ${this.buildEntityPickerField('Averages entity', 'entity', this._config.entity, 'averages')} 17 | ${this.buildSwitchField('Display header', 'display_header', this._config.display_header)} 18 | ${this.buildSelectField('Average format', 'average_format', [{value: 'full', label: 'Full'}, {value: 'short', label: 'Short'}],this._config.average_format)} 19 | ${this.buildSwitchField('Display class average', 'display_class_average', this._config.display_class_average)} 20 | ${this.buildSwitchField('Compare with class average', 'compare_with_class_average', this._config.compare_with_class_average)} 21 | ${this.buildTextField('Compare with ratio', 'compare_with_ratio', this._config.compare_with_ratio, '')} 22 | ${this.buildSwitchField('Display class min', 'display_class_min', this._config.display_class_min)} 23 | ${this.buildSwitchField('Display class max', 'display_class_max', this._config.display_class_max)} 24 | ${this.buildSwitchField('Display overall average', 'display_overall_average', this._config.display_overall_average, true)} 25 | ${this.buildSwitchField('Hide period switch', 'hide_period_switch', this._config.hide_period_switch, false)} 26 | ${this.buildDefaultPeriodSelectField('Default period', 'default_period', 'averages', this._config.default_period, 'current', false)} 27 | `; 28 | } 29 | } 30 | 31 | customElements.define("pronote-averages-card-editor", PronoteAveragesCardEditor); 32 | -------------------------------------------------------------------------------- /src/editors/base-editor.js: -------------------------------------------------------------------------------- 1 | const LitElement = Object.getPrototypeOf( 2 | customElements.get("ha-panel-lovelace") 3 | ); 4 | const html = LitElement.prototype.html; 5 | const css = LitElement.prototype.css; 6 | 7 | class BasePronoteCardEditor extends LitElement { 8 | static get properties() { 9 | return { 10 | hass: {}, 11 | _config: {}, 12 | }; 13 | } 14 | 15 | setConfig(config) { 16 | this._config = config; 17 | this.loadEntityPicker(); 18 | } 19 | 20 | _valueChanged(ev) { 21 | const _config = Object.assign({}, this._config); 22 | 23 | if (typeof ev.target.__checked !== 'undefined') { 24 | _config[ev.target.configValue] = ev.target.__checked; 25 | } else { 26 | _config[ev.target.configValue] = ev.target.value == '' ? null : ev.target.value; 27 | } 28 | 29 | this._config = _config; 30 | 31 | const event = new CustomEvent("config-changed", { 32 | detail: { config: _config }, 33 | bubbles: true, 34 | composed: true, 35 | }); 36 | this.dispatchEvent(event); 37 | } 38 | 39 | buildSelectField(label, config_key, options, value, default_value) { 40 | let selectOptions = []; 41 | for (let i = 0; i < options.length; i++) { 42 | let currentOption = options[i]; 43 | selectOptions.push(html`${currentOption.label}`); 44 | } 45 | 46 | return html` 47 | ev.stopPropagation()} 53 | > 54 | ${selectOptions} 55 | 56 | ` 57 | } 58 | 59 | buildDefaultPeriodSelectField(label, config_key, period_sensor_key, value, default_value = 'current', allow_all_periods = true) { 60 | if (!this._config.entity) { 61 | return html``; 62 | } 63 | let sensor_prefix = this._config.entity.split('_'+period_sensor_key)[0]; 64 | let active_periods = this.hass.states[`${sensor_prefix}_active_periods`].attributes['periods']; 65 | 66 | let options = []; 67 | if (allow_all_periods) { 68 | options.push({value:'all', label: 'All'}); 69 | } 70 | options.push({value:'current', label: 'Current'}); 71 | for (let period of active_periods) { 72 | options.push({value: period.id, label: period.name}); 73 | } 74 | 75 | return this.buildSelectField(label, config_key, options, value, default_value); 76 | } 77 | 78 | buildSwitchField(label, config_key, value, default_value) { 79 | if (typeof value !== 'boolean') { 80 | value = default_value; 81 | } 82 | 83 | return html` 84 | 85 | 86 | 92 | 93 | `; 94 | } 95 | 96 | buildNumberField(label, config_key, value, default_value, step) { 97 | return html` 98 | 104 | `; 105 | } 106 | 107 | buildTextField(label, config_key, value, default_value) { 108 | return html` 109 | 116 | `; 117 | } 118 | 119 | buildEntityPickerField(label, config_key, value, filter) { 120 | const entityFilter = new RegExp("pronote_[a-z_]+_"+filter); 121 | 122 | return html` 123 | 133 | ` 134 | } 135 | 136 | async loadEntityPicker() { 137 | if (window.customElements.get("ha-entity-picker")) { 138 | return; 139 | } 140 | 141 | const ch = await window.loadCardHelpers(); 142 | const c = await ch.createCardElement({ type: "entities", entities: [] }); 143 | await c.constructor.getConfigElement(); 144 | } 145 | 146 | static get styles() { 147 | return css` 148 | ha-selector-boolean { 149 | display: block; 150 | padding-top: 20px; 151 | clear: right; 152 | } 153 | ha-selector-boolean > ha-switch { 154 | float: right; 155 | } 156 | ha-select, ha-textfield { 157 | clear: right; 158 | width: 100%; 159 | padding-top: 15px; 160 | } 161 | `; 162 | } 163 | } 164 | 165 | export default BasePronoteCardEditor; -------------------------------------------------------------------------------- /src/editors/delays-card-editor.js: -------------------------------------------------------------------------------- 1 | import BasePronoteCardEditor from "./base-editor"; 2 | 3 | const LitElement = Object.getPrototypeOf( 4 | customElements.get("ha-panel-lovelace") 5 | ); 6 | 7 | const html = LitElement.prototype.html; 8 | 9 | class PronoteDelaysCardEditor extends BasePronoteCardEditor { 10 | render() { 11 | if (!this.hass || !this._config) { 12 | return html``; 13 | } 14 | 15 | return html` 16 | ${this.buildEntityPickerField('Delays entity', 'entity', this._config.entity, 'delays')} 17 | ${this.buildSwitchField('Display header', 'display_header', this._config.display_header)} 18 | ${this.buildNumberField('Max delays', 'max_delays', this._config.max_delays)} 19 | ${this.buildSwitchField('Hide period switch', 'hide_period_switch', this._config.hide_period_switch, false)} 20 | ${this.buildDefaultPeriodSelectField('Default period', 'default_period', 'delays', this._config.default_period)} 21 | `; 22 | } 23 | } 24 | 25 | customElements.define("pronote-delays-card-editor", PronoteDelaysCardEditor); 26 | -------------------------------------------------------------------------------- /src/editors/evaluations-card-editor.js: -------------------------------------------------------------------------------- 1 | import BasePronoteCardEditor from "./base-editor"; 2 | 3 | const LitElement = Object.getPrototypeOf( 4 | customElements.get("ha-panel-lovelace") 5 | ); 6 | 7 | const html = LitElement.prototype.html; 8 | 9 | class PronoteEvaluationsCardEditor extends BasePronoteCardEditor { 10 | render() { 11 | if (!this.hass || !this._config) { 12 | return html``; 13 | } 14 | 15 | return html` 16 | ${this.buildEntityPickerField('Evaluations entity', 'entity', this._config.entity, 'evaluations')} 17 | ${this.buildSwitchField('Display header', 'display_header', this._config.display_header)} 18 | ${this.buildSwitchField('Display description', 'display_description', this._config.display_description)} 19 | ${this.buildSwitchField('Display teacher', 'display_teacher', this._config.display_teacher)} 20 | ${this.buildSwitchField('Display date', 'display_date', this._config.display_date)} 21 | ${this.buildSwitchField('Display comment', 'display_comment', this._config.display_comment)} 22 | ${this.buildSwitchField('Display coefficient', 'display_coefficient', this._config.display_coefficient)} 23 | ${this.buildNumberField('Max evaluations', 'max_evaluations', this._config.max_evaluations)} 24 | ${this.buildSwitchField('Hide period switch', 'hide_period_switch', this._config.hide_period_switch, false)} 25 | ${this.buildDefaultPeriodSelectField('Default period', 'default_period', 'evaluations', this._config.default_period)} 26 | `; 27 | } 28 | } 29 | 30 | customElements.define("pronote-evaluations-card-editor", PronoteEvaluationsCardEditor); 31 | -------------------------------------------------------------------------------- /src/editors/grades-card-editor.js: -------------------------------------------------------------------------------- 1 | import BasePronoteCardEditor from "./base-editor"; 2 | 3 | const LitElement = Object.getPrototypeOf( 4 | customElements.get("ha-panel-lovelace") 5 | ); 6 | 7 | const html = LitElement.prototype.html; 8 | 9 | class PronoteGradesCardEditor extends BasePronoteCardEditor { 10 | render() { 11 | if (!this.hass || !this._config) { 12 | return html``; 13 | } 14 | 15 | return html` 16 | ${this.buildEntityPickerField('Grades entity', 'entity', this._config.entity, 'grades')} 17 | ${this.buildSwitchField('Display header', 'display_header', this._config.display_header)} 18 | ${this.buildSwitchField('Display date', 'display_date', this._config.display_date)} 19 | ${this.buildSwitchField('Display comment', 'display_comment', this._config.display_comment)} 20 | ${this.buildSwitchField('Display class average', 'display_class_average', this._config.display_class_average)} 21 | ${this.buildSwitchField('Compare with class average', 'compare_with_class_average', this._config.compare_with_class_average)} 22 | ${this.buildSelectField('Grade format', 'grade_format', [{value: 'full', label: 'Full'}, {value: 'short', label: 'Short'}], this._config.grade_format)} 23 | ${this.buildSwitchField('Display coefficient', 'display_coefficient', this._config.display_coefficient)} 24 | ${this.buildSwitchField('Display class min', 'display_class_min', this._config.display_class_min)} 25 | ${this.buildSwitchField('Display class max', 'display_class_max', this._config.display_class_max)} 26 | ${this.buildSwitchField('Display new grade notice', 'display_new_grade_notice', this._config.display_new_grade_notice)} 27 | ${this.buildNumberField('Max grades', 'max_grades', this._config.max_grades)} 28 | ${this.buildSwitchField('Hide period switch', 'hide_period_switch', this._config.hide_period_switch, false)} 29 | ${this.buildDefaultPeriodSelectField('Default period', 'default_period', 'grades', this._config.default_period)} 30 | `; 31 | } 32 | } 33 | 34 | customElements.define("pronote-grades-card-editor", PronoteGradesCardEditor); 35 | -------------------------------------------------------------------------------- /src/editors/homework-card-editor.js: -------------------------------------------------------------------------------- 1 | import BasePronoteCardEditor from "./base-editor"; 2 | 3 | const LitElement = Object.getPrototypeOf( 4 | customElements.get("ha-panel-lovelace") 5 | ); 6 | 7 | const html = LitElement.prototype.html; 8 | 9 | class PronoteHomeworkCardEditor extends BasePronoteCardEditor { 10 | render() { 11 | if (!this.hass || !this._config) { 12 | return html``; 13 | } 14 | 15 | return html` 16 | ${this.buildEntityPickerField('Homework entity', 'entity', this._config.entity, 'homework')} 17 | ${this.buildSwitchField('Display header', 'display_header', this._config.display_header)} 18 | ${this.buildSwitchField('Current week only', 'current_week_only', this._config.current_week_only)} 19 | ${this.buildSwitchField('Reduce done homework', 'reduce_done_homework', this._config.reduce_done_homework)} 20 | ${this.buildSwitchField('Display done homework', 'display_done_homework', this._config.display_done_homework)} 21 | `; 22 | } 23 | } 24 | 25 | customElements.define("pronote-homework-card-editor", PronoteHomeworkCardEditor); 26 | -------------------------------------------------------------------------------- /src/editors/timetable-card-editor.js: -------------------------------------------------------------------------------- 1 | import BasePronoteCardEditor from "./base-editor"; 2 | 3 | const LitElement = Object.getPrototypeOf( 4 | customElements.get("ha-panel-lovelace") 5 | ); 6 | 7 | const html = LitElement.prototype.html; 8 | 9 | class PronoteTimetableCardEditor extends BasePronoteCardEditor { 10 | render() { 11 | if (!this.hass || !this._config) { 12 | return html``; 13 | } 14 | 15 | return html` 16 | ${this.buildEntityPickerField('Timetable entity', 'entity', this._config.entity, '(period_s|today_s|tomorrow_s|next_day_s)_timetable')} 17 | ${this.buildSwitchField('Display header', 'display_header', this._config.display_header, true)} 18 | ${this.buildSwitchField('Current week only', 'current_week_only', this._config.current_week_only, false)} 19 | ${this.buildNumberField('Max days', 'max_days', this._config.max_days, null, 1)} 20 | ${this.buildSwitchField('Display classroom', 'display_classroom', this._config.display_classroom, true)} 21 | ${this.buildSwitchField('Display day hours', 'display_day_hours', this._config.display_day_hours, true)} 22 | ${this.buildSwitchField('Display lunch break', 'display_lunch_break', this._config.display_lunch_break, true)} 23 | ${this.buildSwitchField('Dim ended lessons', 'dim_ended_lessons', this._config.dim_ended_lessons, true)} 24 | ${this.buildSwitchField('Enable slider', 'enable_slider', this._config.enable_slider, false)} 25 | ${this.buildSwitchField('Display free time slots', 'display_free_time_slots', this._config.display_free_time_slots, true)} 26 | `; 27 | } 28 | } 29 | 30 | customElements.define("pronote-timetable-card-editor", PronoteTimetableCardEditor); 31 | -------------------------------------------------------------------------------- /src/pronote.ts: -------------------------------------------------------------------------------- 1 | import './cards/timetable-card.js' 2 | import './cards/homework-card.js' 3 | import './cards/grades-card.js' 4 | import './cards/averages-card.js' 5 | import './cards/evaluations-card.js' 6 | import './cards/absences-card.js' 7 | import './cards/delays-card.js' 8 | 9 | import './editors/timetable-card-editor.js' 10 | import './editors/homework-card-editor.js' 11 | import './editors/grades-card-editor.js' 12 | import './editors/evaluations-card-editor.js' 13 | import './editors/delays-card-editor.js' 14 | import './editors/averages-card-editor.js' 15 | import './editors/absences-card-editor.js' -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "lib": ["es2017", "dom", "dom.iterable"], 7 | "plugins": [ 8 | { 9 | "name": "ts-lit-plugin" 10 | } 11 | ], 12 | "noEmit": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "strict": true, 17 | "noImplicitAny": false, 18 | "skipLibCheck": true, 19 | "resolveJsonModule": true, 20 | "experimentalDecorators": true, 21 | "sourceMap": true, 22 | "allowSyntheticDefaultImports": true 23 | }, 24 | "include": ["src/*", "src/**/*"] 25 | } --------------------------------------------------------------------------------