diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..73816f1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# Root editor config file +root = true + +# Common settings +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +# python, js indentation settings +[{*.py,*.js,*.vue,*.css,*.scss,*.html}] +indent_style = tab +indent_size = 4 +max_line_length = 99 + +# JSON files - mostly doctype schema files +[{*.json}] +insert_final_newline = false +indent_style = space +indent_size = 1 diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..2012ee7 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,125 @@ +{ + "env": { + "browser": true, + "node": true, + "es2022": true + }, + "parserOptions": { + "sourceType": "module" + }, + "extends": "eslint:recommended", + "rules": { + "indent": "off", + "brace-style": "off", + "no-mixed-spaces-and-tabs": "off", + "no-useless-escape": "off", + "space-unary-ops": ["error", { "words": true }], + "linebreak-style": "off", + "quotes": ["off"], + "semi": "off", + "camelcase": "off", + "no-unused-vars": "off", + "no-console": ["warn"], + "no-extra-boolean-cast": ["off"], + "no-control-regex": ["off"], + }, + "root": true, + "globals": { + "buzz": true, + "frappe": true, + "Vue": true, + "SetVueGlobals": true, + "__": true, + "repl": true, + "Class": true, + "locals": true, + "cint": true, + "cstr": true, + "cur_frm": true, + "cur_dialog": true, + "cur_page": true, + "cur_list": true, + "cur_tree": true, + "msg_dialog": true, + "is_null": true, + "in_list": true, + "has_common": true, + "posthog": true, + "has_words": true, + "validate_email": true, + "open_web_template_values_editor": true, + "validate_name": true, + "validate_phone": true, + "validate_url": true, + "get_number_format": true, + "format_number": true, + "format_currency": true, + "comment_when": true, + "open_url_post": true, + "toTitle": true, + "lstrip": true, + "rstrip": true, + "strip": true, + "strip_html": true, + "replace_all": true, + "flt": true, + "precision": true, + "CREATE": true, + "AMEND": true, + "CANCEL": true, + "copy_dict": true, + "get_number_format_info": true, + "strip_number_groups": true, + "print_table": true, + "Layout": true, + "web_form_settings": true, + "$c": true, + "$a": true, + "$i": true, + "$bg": true, + "$y": true, + "$c_obj": true, + "refresh_many": true, + "refresh_field": true, + "toggle_field": true, + "get_field_obj": true, + "get_query_params": true, + "unhide_field": true, + "hide_field": true, + "set_field_options": true, + "getCookie": true, + "getCookies": true, + "get_url_arg": true, + "md5": true, + "$": true, + "jQuery": true, + "moment": true, + "hljs": true, + "Awesomplete": true, + "Sortable": true, + "Showdown": true, + "Taggle": true, + "Gantt": true, + "Slick": true, + "Webcam": true, + "PhotoSwipe": true, + "PhotoSwipeUI_Default": true, + "io": true, + "JsBarcode": true, + "L": true, + "Chart": true, + "DataTable": true, + "Cypress": true, + "cy": true, + "it": true, + "describe": true, + "expect": true, + "context": true, + "before": true, + "beforeEach": true, + "after": true, + "qz": true, + "localforage": true, + "extend_cscript": true + } +} diff --git a/.github/images/booking-details-page.png b/.github/images/booking-details-page.png new file mode 100644 index 0000000..c4763d0 Binary files /dev/null and b/.github/images/booking-details-page.png differ diff --git a/.github/images/booking-form.png b/.github/images/booking-form.png new file mode 100644 index 0000000..86aa6a2 Binary files /dev/null and b/.github/images/booking-form.png differ diff --git a/.github/images/fe-event-main-form.png b/.github/images/fe-event-main-form.png new file mode 100644 index 0000000..9f9705d Binary files /dev/null and b/.github/images/fe-event-main-form.png differ diff --git a/.github/images/sponsorship-management.png b/.github/images/sponsorship-management.png new file mode 100644 index 0000000..f21621e Binary files /dev/null and b/.github/images/sponsorship-management.png differ diff --git a/.github/images/sponsorship-payment.png b/.github/images/sponsorship-payment.png new file mode 100644 index 0000000..6afe12e Binary files /dev/null and b/.github/images/sponsorship-payment.png differ diff --git a/.github/images/ticket-types-and-add-ons.png b/.github/images/ticket-types-and-add-ons.png new file mode 100644 index 0000000..6e8c020 Binary files /dev/null and b/.github/images/ticket-types-and-add-ons.png differ diff --git a/.github/images/ticket-updates.png b/.github/images/ticket-updates.png new file mode 100644 index 0000000..2524340 Binary files /dev/null and b/.github/images/ticket-updates.png differ diff --git a/.github/instructions/frappe-ui.instructions.md b/.github/instructions/frappe-ui.instructions.md new file mode 100644 index 0000000..ed89c5e --- /dev/null +++ b/.github/instructions/frappe-ui.instructions.md @@ -0,0 +1,1550 @@ +--- +applyTo: '**/*.vue' +--- + +### General Backend API Calls + +```vue + + + +``` + +### Document Resource + +```vue + +``` + +Example API: + +```vue +todo.doc // doc returned from request +todo.reload() // reload the doc + +// update options +todo.update({ + doctype: '', + name: '' +}) + +todo.get // doc resource +todos.get.loading // true when data is being fetched +todos.get.error // error that occurred from making the request +todos.get.promise // promise object of the request, can be awaited + +// resource to set value(s) on the document +todos.setValue +todos.setValue.submit({ + // field value pairs to set + status: 'Closed', + description: 'Updated description' +}) + +// same as setValue but debounced +todos.setValueDebounced +// will run once after 500ms +todos.setValueDebounced.submit({ + description: 'Updated description' +}) + +// resource to delete the document +todos.delete +todos.delete.submit() + +// if whitelistedMethods is defined +// you get a resource for each whitelisted method +todos.sendEmail +todos.sendEmail.submit +todos.sendEmail.loading +``` + +### List Resource + +List Resource is a wrapper on top of [Resource](./Resource.story.md) for working +with lists. This feature only works with a Frappe Framework backend as of now. + +## Usage + +A list resource knows how to fetch records of a DocType from a Frappe Framework +backend so there is no need to specify the url. Instead you only define +`doctype`, `fields`, `filters`, etc. You also get methods like `next()`, +`setValue()`, etc. + +```vue + + +``` + +## Options API + +You can also define resources if you are using Options API. You need to register +the `resourcesPlugin` first. + +**main.js** + +```js +import { resourcesPlugin } from 'frappe-ui' +app.use(resourcesPlugin) +``` + +In your .vue file, you can declare all your resources under the resources key as +functions. The resource object will be available on `this.$resources.[name]`. In +the following example, `this.$resources.todos` is the resource object. + +**Component.vue** + +```vue + +``` + +## List of Options and API + +Here is the list of all options and APIs that are available on a list resource. + +### Options + +```js +let todos = uesList({ + // name of the doctype + doctype: 'ToDo', + + // list of fields + fields: ['name', 'description', 'status', ...], + + // object of filters to apply + filters: { + status: 'Open' + }, + + // the order in which records must be sorted + orderBy: 'creation desc', + + // index from which records should be fetched + // default value is 0 + start: 0, + + // number of records to fetch in a single request + // default value is 20 + pageLength: 20, + + // parent doctype when you are fetching records of a child doctype + parent: null, + + // set to 1 to enable debugging of list query + debug: 0, + + // cache key to cache the resource + // can be a string + cacheKey: 'todos', + // or an array that can be serialized + cacheKey: ['todos', 'faris@frappe.io'], + + // default value for url is "frappe.client.get_list" + // specify url if you want to use a custom API method + url: 'todo_app.api.get_todos', + + // make the first request automatically + auto: true, + + // events + // error can occur from failed request + onError(error) { + + }, + // on successful response + onSuccess(data) { + + }, + // transform data before setting it + transform(data) { + for (let d of data) { + d.open = false + } + return data + }, + // other events + fetchOne: { + onSuccess() {}, + onError() {} + }, + insert: { + onSuccess() {}, + onError() {} + }, + delete: { + onSuccess() {}, + onError() {} + }, + setValue: { + onSuccess() {}, + onError() {} + }, + runDocMethod: { + onSuccess() {}, + onError() {} + }, +}) +``` + +### API + +A list resource is made up of multiple individual resources. In our running +example, the resource object that fetches the list is at `todos.list`. So all +the [properties of a resource](./Resource.story.md) are available on this +object. Similarly, there are resources for `fetchOne`, `setValue`, `insert`, +`delete`, and `runDocMethod`. + +```js +let todos = useList({...}) + +todos.data // data returned from request +todos.originalData // response data before being transformed +todos.reload() // reload the existing list +todos.next() // fetch the next page +todos.hasNextPage // whether there is next page to fetch + +// update list options +todos.update({ + fields: ['*'], + filters: { + status: 'Closed' + } +}) + +todos.data // list resource +todos.loading // true when data is being fetched +todos.error // error that occurred from making the request +todos.promise // promise object of the request, can be awaited + +// resource to fetch and update a single record in the list +todos.fetchOne +// pass the name of the record to fetch that record and update the list +todos.fetchOne.submit(name) + +// resource to set value(s) for a single record in the list +todos.setValue +todos.setValue.submit({ + // id of the record + name: '', + // field value pairs to set + status: 'Closed', + description: 'Updated description' +}) + +// resource to insert a new record in the list +todos.insert +todos.insert.submit({ + description: 'New todo' +}) + +// resource to delete a single record +todos.delete +todos.delete.submit(name) + +// resource to run a doc method +todos.runDocMethod +todos.runDocMethod.submit({ + // name of the doc method + method: 'send_email', + // name of the record + name: '', + // params to pass to the method + email: 'test@example.com' +}) +``` + + +### List View Component + +### Story + +```vue + + + +``` + +## Props + +### Row Key + +`row-key` is a unique key which is used to identify each row in the list. It is +required to be passed in the `row` object. + +### Column + +1. `label` & `key` is required in column object. + +2. `width` is optional and it is used to set column width in list + + 1. If you need a column to be `3` times a default column then add `3`. if + width is not mentioned default will be `1` + 2. You can also add custom width in px and rem e.g `300px` or `12rem` + 3. Combination of both can also be used. + +3. `align` is also optional. You can change the alignment of the content in the + column by setting it as. + + 1. `start` or `left` (default) + 2. `center` or `middle` + 3. `end` or `right` + +4. You can add more attributes which can be used to render custom column header + items. + +### Row + +1. The row object must contain a unique_key which was mentioned in ListView + `row-key` +2. Then you can add the row fields as key value pairs and each field can be an + object or a string (to handle custom rendering) + + ``` + { + // unique_key 'id' + id: 1, + + // row fields + name: 'John Doe', + age: 25, + email: 'john@doe.com', + } + ``` + + E.g field value as an object (to handle custom rendering), but make sure it + has a `label` attribute which holds the actual value to be shown + + ``` + row: { + name: { + label: 'John Doe', + image: '/johndoe.jpg', + }, + age: 25, + status: { + label: 'Active', + color: 'green' + } + } + ``` + +### Grouped Rows + +To render grouped rows, you must provide `rows` in the following format: + +``` +[ + { + group: 'Group Title 1', + collapsed: false, + rows: [ + {id: 1, key1: value1, key2: value2, ...}, + {id: 2, key1: value1, key2: value2, ...}, + ] + }, + { + group: 'Group Title 2', + collapsed: false, + rows: [ + {id: 3, key1: value1, key2: value2, ...}, + {id: 4, key1: value1, key2: value2, ...}, + ] + }, +] +``` + +### Options + +1. If you want to route using router-link just add a `getRowRoute` function + which returns a route object + + `getRowRoute: (row) => ({ name: 'User', params: { userId: row.id } })` + +2. if you need to do some action add a `onRowClick` event handler + + `onRowClick: (row) => console.log(row.label + ' was clicked')` + +3. selectable (Boolean) - if true, checkbox will be shown in header and rows, to + select/multiselect rows and perform some action on them - default is true +4. showTooltip (Boolean) - if true, tooltip will be shown on hover of row - + default is true +5. resizeColumn (Boolean) - if true, column can be resized by dragging the + resizer on the right side of the column header - default is false + +--- + +### Selection Banner (Will be shown when selectable (default is true) is true) + +**Without custom action buttons:** +image + +**With custom action buttons:** +image + +``` + + + +``` + +You can also make your own custom selection banner + +image + +``` + +
Custom Banner
+
+``` + + +### Badge Component + +#### Story + +```vue + + + +``` + + +### FormControl Component + + +#### Story + +```vue + + + +``` + + +### Toasts + +Example: + +```vue + +``` + + +### Dropdown Component + +The icon is name of a lucide icon. + +#### Story + +```vue + + + +``` + +### Dialog Component + + +#### Story + +```vue + + + +``` diff --git a/.github/screenshots/tax-exclusive.png b/.github/screenshots/tax-exclusive.png new file mode 100644 index 0000000..b95a1d6 Binary files /dev/null and b/.github/screenshots/tax-exclusive.png differ diff --git a/.github/screenshots/tax-inclusive.png b/.github/screenshots/tax-inclusive.png new file mode 100644 index 0000000..0e0dc26 Binary files /dev/null and b/.github/screenshots/tax-inclusive.png differ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f970288 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,108 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + +concurrency: + group: main-event_manager-${{ github.event.number }} + cancel-in-progress: true + +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + name: Server + + services: + redis-cache: + image: redis:alpine + ports: + - 13000:6379 + redis-queue: + image: redis:alpine + ports: + - 11000:6379 + mariadb: + image: mariadb:10.6 + env: + MYSQL_ROOT_PASSWORD: root + ports: + - 3306:3306 + options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + + steps: + - name: Clone + uses: actions/checkout@v3 + + - name: Find tests + run: | + echo "Finding tests" + grep -rn "def test" > /dev/null + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.14' + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 24 + check-latest: true + + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py', '**/setup.cfg') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: 'echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT' + + - uses: actions/cache@v4 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Install MariaDB Client + run: | + sudo apt update + sudo apt-get install mariadb-client + + - name: Setup + run: | + pip install frappe-bench + bench init --skip-redis-config-generation --skip-assets --python "$(which python)" ~/frappe-bench + mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'" + mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" + + - name: Install + working-directory: /home/runner/frappe-bench + run: | + bench get-app event_manager $GITHUB_WORKSPACE + bench get-app payments + bench setup requirements --dev + bench new-site --db-root-password root --admin-password admin test_site + bench --site test_site install-app event_manager + bench build + env: + CI: 'Yes' + + - name: Run Tests + working-directory: /home/runner/frappe-bench + run: | + bench --site test_site set-config allow_tests true + bench --site test_site run-tests --app event_manager + env: + TYPE: server diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 0000000..8e93d3b --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,63 @@ +name: Linters + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + linter: + name: 'Frappe Linter' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + cache: pip + - uses: pre-commit/action@v3.0.0 + + - name: Download Semgrep rules + run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules + + - name: Run Semgrep rules + run: | + pip install semgrep + semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness + + deps-vulnerable-check: + name: 'Vulnerable Dependency Check' + runs-on: ubuntu-latest + + steps: + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - uses: actions/checkout@v4 + + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Install and run pip-audit + run: | + pip install pip-audit + cd ${GITHUB_WORKSPACE} + pip-audit --desc on . diff --git a/.github/workflows/pr-tittle-check.yml b/.github/workflows/pr-tittle-check.yml new file mode 100644 index 0000000..c0648fb --- /dev/null +++ b/.github/workflows/pr-tittle-check.yml @@ -0,0 +1,74 @@ +name: PR Title Check + +on: + pull_request: + types: + - opened + - reopened + - synchronize + - edited + +permissions: + pull-requests: write + +jobs: + validate-pr-title: + runs-on: ubuntu-slim + steps: + - name: Validate PR title + id: validate + uses: amannn/action-semantic-pull-request@v6.1.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: true + with: + # Default types from: https://github.com/commitizen/conventional-commit-types + requireScope: false + wip: false + + - name: Print Failure Documentation + if: steps.validate.outcome == 'failure' + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + echo "## ❌ PR Title Validation Failed" + echo "" + echo "**Current title:** \`$PR_TITLE\`" + echo "" + echo "Use a conventional commit style so we can keep a clear history. Fix it like this:" + echo "" + echo "### 📋 Format" + echo "" + echo "\`type: description\` or \`type(scope): description\`" + echo "" + echo "### 🏷️ Allowed types" + echo "" + echo "| Type | Use for |" + echo "|----------|--------|" + echo "| feat | New feature or user-facing change |" + echo "| fix | Bug fix |" + echo "| docs | Documentation only |" + echo "| style | Formatting, no logic change |" + echo "| refactor | Code change (no new feature or bug fix) |" + echo "| perf | Performance improvement |" + echo "| test | Adding or updating tests |" + echo "| build | Build system or dependencies |" + echo "| ci | CI configuration |" + echo "| chore | Other (maintenance, tooling) |" + echo "" + echo "### 🎯 Examples" + echo "" + echo "- \`feat: add user authentication\`" + echo "- \`feat(api): add payment endpoint\`" + echo "- \`fix: resolve login timeout\`" + echo "- \`chore: update Python dependencies\`" + echo "" + echo "### ❌ Fix these mistakes" + echo "" + echo "- \`Add user login\` → \`feat: add user authentication\`" + echo "- \`Fixed the bug\` → \`fix: resolve checkout error\`" + echo "- \`feat:add feature\` → \`feat: add feature\` (space after colon)" + echo "" + echo "---" + echo "Edit the PR title and this check will re-run automatically." + exit 1 diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml new file mode 100644 index 0000000..c987bfb --- /dev/null +++ b/.github/workflows/typecheck.yml @@ -0,0 +1,41 @@ +name: Dashboard TypeScript + +on: + push: + branches: + - develop + paths: + - "dashboard/**" + - ".github/workflows/typecheck.yml" + pull_request: + paths: + - "dashboard/**" + - ".github/workflows/typecheck.yml" + +concurrency: + group: typecheck-event_manager-${{ github.event.number || github.sha }} + cancel-in-progress: true + +jobs: + typecheck: + name: TypeScript + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "24" + cache: "yarn" + cache-dependency-path: dashboard/yarn.lock + + - name: Install dependencies + working-directory: dashboard + run: yarn install --frozen-lockfile + + - name: Type check + working-directory: dashboard + run: yarn typecheck \ No newline at end of file diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml new file mode 100644 index 0000000..f1d8085 --- /dev/null +++ b/.github/workflows/ui-tests.yml @@ -0,0 +1,169 @@ +name: UI Tests + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +concurrency: + group: ui-tests-event_manager-${{ github.event.number || github.ref }} + cancel-in-progress: true + +jobs: + ui-tests: + runs-on: ubuntu-latest + timeout-minutes: 60 + name: Playwright E2E Tests + + services: + redis-cache: + image: redis:alpine + ports: + - 13000:6379 + redis-queue: + image: redis:alpine + ports: + - 11000:6379 + mariadb: + image: mariadb:10.6 + env: + MYSQL_ROOT_PASSWORD: root + ports: + - 3306:3306 + options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + + steps: + - name: Clone + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.14" + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 24 + check-latest: true + + - name: Add to Hosts + run: echo "127.0.0.1 event_manager.test" | sudo tee -a /etc/hosts + + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py', '**/setup.cfg') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: 'echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT' + + - name: Cache yarn + uses: actions/cache@v4 + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('**/package.json') }} + restore-keys: | + ${{ runner.os }}-playwright- + + - name: Install MariaDB Client + run: | + sudo apt update + sudo apt-get install mariadb-client + + - name: Setup Bench + run: | + pip install frappe-bench + bench init --skip-redis-config-generation --skip-assets --python "$(which python)" ~/frappe-bench + mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'" + mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" + + - name: Install event_manager + working-directory: /home/runner/frappe-bench + run: | + bench get-app payments + bench get-app event_manager $GITHUB_WORKSPACE + bench setup requirements --dev + bench new-site --db-root-password root --admin-password admin event_manager.test + bench --site event_manager.test install-app event_manager + bench build + env: + CI: "Yes" + + - name: Configure Site for UI Tests + working-directory: /home/runner/frappe-bench + run: | + bench --site event_manager.test set-config allow_tests true + bench --site event_manager.test set-config host_name "http://event_manager.test:8000" + + - name: Create Test User + working-directory: /home/runner/frappe-bench + run: bench --site event_manager.test add-user testuser@event_manager.test --first-name Test --last-name User --password Test@123 --add-role "System Manager" + + - name: Start Frappe Server + working-directory: /home/runner/frappe-bench + run: | + # Disable watch and schedule to reduce resource usage + sed -i 's/^watch:/# watch:/g' Procfile + sed -i 's/^schedule:/# schedule:/g' Procfile + + # Start bench in background + bench start &> bench_start.log & + + # Wait for server to be ready + echo "Waiting for Frappe server to start..." + timeout 60 bash -c 'until curl -s http://event_manager.test:8000 > /dev/null; do sleep 2; done' + echo "Frappe server is ready!" + + - name: Install Playwright + run: | + npm install + npx playwright install --with-deps chromium + + - name: Run Playwright Tests + run: npx playwright test + env: + BASE_URL: http://event_manager.test:8000 + FRAPPE_USER: testuser@event_manager.test + FRAPPE_PASSWORD: Test@123 + + - name: Upload Playwright Report + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 7 + + - name: Upload Test Results + uses: actions/upload-artifact@v4 + if: failure() + with: + name: test-results + path: test-results/ + retention-days: 7 + + - name: Show Bench Logs on Failure + if: failure() + working-directory: /home/runner/frappe-bench + run: | + echo "=== Bench Start Log ===" + cat bench_start.log || true + echo "" + echo "=== Frappe Logs ===" + cat logs/*.log || true \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89ff80d --- /dev/null +++ b/.gitignore @@ -0,0 +1,67 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class +*.pyc +*.py~ + +# Distribution / packaging +.Python +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +tags +MANIFEST + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Dependency directories +node_modules/ +jspm_packages/ + +# IDEs and editors +.vscode/ +.vs/ +.idea/ +.kdev4/ +*.kdev4 +*.DS_Store +*.swp +*.comp.js +.wnf-lang-status +*debug.log + +# Helix Editor +.helix/ + +# Aider AI Chat +.aider* + +buzz/public/dashboard/ +buzz/public/node_modules +buzz/www/dashboard.html + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ +/e2e/.auth/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d1d862d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "dashboard/frappe-ui"] + path = dashboard/frappe-ui + url = https://github.com/frappe/frappe-ui.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..bb6802f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,69 @@ +exclude: 'node_modules|.git' +default_stages: [pre-commit] +fail_fast: false + + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + files: "event_manager.*" + exclude: ".*json$|.*txt$|.*csv|.*md|.*svg" + - id: check-merge-conflict + - id: check-ast + - id: check-json + - id: check-toml + - id: check-yaml + - id: debug-statements + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.1 + hooks: + - id: ruff + name: "Run ruff import sorter" + args: ["--select=I", "--fix"] + + - id: ruff + name: "Run ruff linter" + + - id: ruff-format + name: "Run ruff formatter" + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.7.1 + hooks: + - id: prettier + types_or: [javascript, vue, scss] + # Ignore any files that might contain jinja / bundles + exclude: | + (?x)^( + event_manager/public/dist/.*| + .*node_modules.*| + .*boilerplate.*| + event_manager/templates/includes/.*| + event_manager/public/js/lib/.* + )$ + + + - repo: https://github.com/pre-commit/mirrors-eslint + rev: v8.44.0 + hooks: + - id: eslint + types_or: [javascript] + args: ['--quiet'] + # Ignore any files that might contain jinja / bundles + exclude: | + (?x)^( + event_manager/public/dist/.*| + cypress/.*| + .*node_modules.*| + .*boilerplate.*| + event_manager/templates/includes/.*| + event_manager/public/js/lib/.* + )$ + +ci: + autoupdate_schedule: weekly + skip: [] + submodules: false diff --git a/AGENTS.md b/AGENTS.md new file mode 120000 index 0000000..681311e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +CLAUDE.md \ No newline at end of file diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..668b338 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,249 @@ +# Pohodex Event Manager App Architecture Notes + +## Overview +- Pohodex Event Manager is a Frappe app for event management with a Vue 3 (FrappeUI) dashboard and Frappe Builder/public pages. +- Backend is organized into Frappe modules: `Pohodex Event Manager`, `Events`, `Ticketing`, `Proposals`. +- Frontend dashboard lives in `dashboard/`, built with Vite and published into `event_manager/public/dashboard` and `event_manager/www/dashboard.html`. + +## Backend Structure +- App bootstrap and hooks: `event_manager/hooks.py` + - Requires `frappe/payments`. + - Scheduler: `event_manager.tasks.unpublish_ticket_types_after_last_date` (daily). + - Doc events: assigns `Pohodex Event Manager User` role on user creation; syncs Speaker Profile display name on User update. + - App icon entry in Desk apps screen. +- Core API: `event_manager/api.py` + - Booking, tickets, add-ons, sponsorship, check-in, languages, translations. +- Payments integration: `event_manager/payments.py` + - Creates `Event Payment` records and generates payment links via the Payments app. +- Utilities: `event_manager/utils.py` + - App-install guard decorator, custom field helpers, role assignment. +- Install/fixtures: + - `event_manager/install.py` creates default Event Categories and zoom integration fields if installed. + - `event_manager/fixtures` and `event_manager/patches` for setup and migrations. + +## Key DocTypes (Data Model) + +### Events Module +- `Pohodex Event Manager Event` (`event_manager/events/doctype/buzz_event`) + - Primary entity; holds schedule, category, venue, ticketing/payment settings, sponsorship settings. + - Creates default `Sponsorship Tier` and `Event Ticket Type` after insert. + - Optional Zoom webinar creation/updates when `zoom_integration` is installed. +- `Event Category`, `Event Venue`, `Event Host` + - Category slug creation; venue location geojson and map embed cleanup. +- `Event Talk`, `Talk Speaker`, `Speaker Profile`, `Event Track` + - Talks can be generated from proposals; speaker profiles tied to User records. +- `Event Sponsor`, `Sponsorship Tier`, `Sponsorship Deck Item` + - Sponsor uniqueness enforced per enquiry. +- `Event Check In` + - Used for check-in records and attendance reporting. +- `Event Payment Gateway` + - Child table for multiple payment gateways per event. +- `Additional Event Page` + - Event-specific extra pages with route validation. + +### Ticketing Module +- `Event Booking` + - Holds attendees, pricing, tax, UTM parameters, custom fields. + - On submit: generates `Event Ticket` documents and applies add-ons/custom fields. +- `Event Ticket` + - Generates QR code, sends ticket email + print format attachment. + - Creates Zoom webinar registration (if enabled). + - Supports transfer/cancellation flows. +- `Event Ticket Type` + - Inventory logic; tracks max tickets and remaining count. +- `Ticket Add-on` + `Ticket Add-on Value` + `Attendee Ticket Add-on` + - Add-on definitions and per-ticket selections. +- `Bulk Ticket Coupon` + - Auto-generates code; limits usage via claimed count. +- `Ticket Cancellation Request` + `Ticket Cancellation Item` + - Cancel booking or specific tickets on acceptance. +- `Additional Field` + - Generic key/value for ticket/booking custom fields. + +### Proposals Module +- `Talk Proposal` + - Maps into `Event Talk` and auto-creates users/speaker profiles if needed. +- `Sponsorship Enquiry` + - Handles approval/payment flow; creates `Event Sponsor` on payment completion. + +### Pohodex Event Manager Module +- `Pohodex Event Manager Custom Field` + - Event-specific custom fields applied to bookings or tickets. +- `Pohodex Event Manager Settings` + - Central configuration for transfer/add-on/cancellation windows. + +## Backend Flows + +### Booking + Payment +1. Dashboard calls `event_manager.api.get_event_booking_data` to load ticket types, add-ons, custom fields, and payment gateways. +2. `event_manager.api.process_booking` creates an `Event Booking` with attendees, add-ons, custom fields, and UTM parameters. +3. If total is 0, booking is auto-submitted; otherwise `Event Payment` is created and a payment URL is returned. +4. On payment authorization, `Event Booking.on_payment_authorized` marks the payment received and submits the booking. +5. Booking submission creates `Event Ticket` records and triggers QR + email flow. + +### Ticket Lifecycle +- Ticket creation generates QR code file and email (with print format attachment). +- Transfers are handled via `event_manager.api.transfer_ticket` with window checks from `Pohodex Event Manager Settings`. +- Add-on preference changes use `event_manager.api.change_add_on_preference` with window checks. +- Cancellation requests are created via `event_manager.api.create_cancellation_request` and are accepted/rejected in Desk. + +### Sponsorships +- Web form submits `Sponsorship Enquiry`. +- Approval moves status to `Payment Pending` and triggers email notification. +- Payment link is generated via `event_manager.api.create_sponsorship_payment_link`. +- Payment authorization creates `Event Sponsor` and marks enquiry as paid. + +### Check-in +- Dashboard scanner validates tickets with `event_manager.api.validate_ticket_for_checkin`. +- Successful check-in creates `Event Check In` and returns event/ticket context. + +## API Surface (Whitelisted) +- Booking: `get_event_booking_data`, `process_booking`, `get_booking_details`, `create_cancellation_request`. +- Ticket actions: `get_ticket_details`, `transfer_ticket`, `change_add_on_preference`. +- Sponsorships: `get_sponsorship_details`, `get_user_sponsorship_inquiries`, `create_sponsorship_payment_link`, `withdraw_sponsorship_enquiry`. +- Check-in: `validate_ticket_for_checkin`, `checkin_ticket`. +- Payments: `get_event_payment_gateways` (plus payment helpers in `event_manager/payments.py`). +- User + i18n: `get_user_info`, `get_enabled_languages`, `update_user_language`, `get_translations`. + +## Reports +- Events: + - `Event Overview` (tickets sold, add-ons sold, sales) in `event_manager/events/report/event_overview`. + - `Event Attendance Summary` with dynamic per-day check-ins and chart in `event_manager/events/report/event_attendance_summary`. +- Ticketing: + - `Event Add-Ons Overview` in `event_manager/ticketing/report/event_add_ons_overview`. + - `Detailed Event Registrations` with dynamic custom fields, add-ons, UTM params in `event_manager/ticketing/report/detailed_event_registrations`. + +## Public Pages + Web Forms +- Dashboard is served via `event_manager/www/dashboard.html` (built from `dashboard/` output). +- Web forms: + - `Apply for Sponsorship` and `Propose a Talk` under `event_manager/event_manager/web_form/`. +- Frappe Builder assets are under `event_manager/builder_files` and `event_manager/public/builder_files` (currently empty stubs/asset containers). + +## Frontend (Vue Dashboard) + +### Entry + Build +- Entry: `dashboard/src/main.js` mounts `App.vue` with router, resources, translation plugin, and socket. +- Build: `dashboard/vite.config.js` outputs to `event_manager/public/dashboard` and updates `event_manager/www/dashboard.html`. +- Base URL: `/dashboard` (router history uses `createWebHistory("/dashboard")`). + +### Routing +- Public-like flows: booking (`/book-tickets/:eventRoute`) and check-in (`/check-in`). +- Account area under `/account`: + - bookings list/details, tickets list/details, sponsorships list/details. +- Guard: `router.beforeEach` checks `event_manager.api.get_user_info` and redirects to `/login` if unauthenticated. + +### Data Access Pattern +- Uses `frappe-ui` `createResource` and `createListResource` for API calls. +- API endpoints map directly to backend whitelisted methods (see API Surface above). +- Session state in `dashboard/src/data/session.js` handles login/logout and cookie-based session user. + +### Key UI Components +- Booking flow: `BookingForm.vue`, `PaymentGatewayDialog.vue`, `BookingSummary.vue`. +- Ticket management: `TicketDetails.vue`, `TicketTransferDialog.vue`, `AddOnPreferenceDialog.vue`, `CancellationRequestDialog.vue`. +- Sponsorship management: `SponsorshipDetails.vue`, `SponsorshipPaymentDialog.vue`, `SponsorLogoUploader.vue`. +- Check-in: `CheckInScanner.vue`, `EventSelector.vue`, `QRScanner.vue`. +- Shared: `Navbar.vue`, `LanguageSwitcher.vue`, `SuccessMessage.vue`. + +### Notable Composables + Utils +- `useTicketValidation` handles check-in validation flow and audio feedback. +- `usePaymentSuccess` handles success banners + confetti and URL cleanup. +- `useBookingFormStorage` keeps draft booking data in localStorage per event. +- `useLanguage` changes user language and reloads translations. + +## Integrations +- Payments app required (gateway link generation + payment receipt recording). +- Zoom integration optional; `Pohodex Event Manager Event` can create/update webinars and register attendees. + +## Permissions + Roles +- Roles: `Pohodex Event Manager User`, `Frontdesk Manager` (fixtures in hooks). +- Check-in APIs restricted to `Frontdesk Manager`. +- Sponsorship details restricted to enquiry owner or users with permission. + +## Key Paths for Feature Work +- Booking logic: `event_manager/api.py`, `event_manager/ticketing/doctype/event_booking`, `dashboard/src/components/BookingForm.vue`. +- Ticket lifecycle: `event_manager/ticketing/doctype/event_ticket`, `dashboard/src/pages/TicketDetails.vue`. +- Sponsorship lifecycle: `event_manager/proposals/doctype/sponsorship_enquiry`, `dashboard/src/pages/SponsorshipDetails.vue`. +- Check-in flow: `event_manager/api.py`, `dashboard/src/pages/CheckInScanner.vue`. +- Event configuration: `event_manager/events/doctype/buzz_event` and related event doctypes. + +## Notes / Gotchas +- Payment gateway selection is event-scoped via `Event Payment Gateway` child rows. +- Event routes are auto-generated when publishing (`Pohodex Event Manager Event`, `Additional Event Page`). +- Custom fields are event-scoped (`Pohodex Event Manager Custom Field`) and can apply to booking or ticket. +- Ticket type availability is enforced at booking time, plus daily auto-unpublish by scheduler. + +## Data Model Diagram (DocTypes + Relationships) +```mermaid +erDiagram + BUZZ_EVENT ||--o{ EVENT_TICKET_TYPE : has + BUZZ_EVENT ||--o{ TICKET_ADD_ON : has + BUZZ_EVENT ||--o{ EVENT_BOOKING : has + BUZZ_EVENT ||--o{ EVENT_SPONSOR : has + BUZZ_EVENT ||--o{ SPONSORSHIP_TIER : has + BUZZ_EVENT ||--o{ BUZZ_CUSTOM_FIELD : has + BUZZ_EVENT ||--o{ EVENT_CHECK_IN : has + BUZZ_EVENT ||--o{ ADDITIONAL_EVENT_PAGE : has + EVENT_BOOKING ||--o{ EVENT_BOOKING_ATTENDEE : has + EVENT_BOOKING ||--o{ ADDITIONAL_FIELD : has + EVENT_BOOKING ||--o{ UTM_PARAMETER : has + EVENT_BOOKING ||--o{ EVENT_TICKET : generates + EVENT_TICKET ||--o{ TICKET_ADD_ON_VALUE : has + EVENT_TICKET ||--o{ ADDITIONAL_FIELD : has + EVENT_TICKET_TYPE ||--o{ EVENT_TICKET : sold_as + ATTENDEE_TICKET_ADD_ON ||--o{ TICKET_ADD_ON_VALUE : has + TICKET_CANCELLATION_REQUEST ||--o{ TICKET_CANCELLATION_ITEM : includes + TICKET_CANCELLATION_REQUEST }o--|| EVENT_BOOKING : cancels + EVENT_CHECK_IN }o--|| EVENT_TICKET : for + SPONSORSHIP_ENQUIRY }o--|| BUZZ_EVENT : for + SPONSORSHIP_ENQUIRY ||--o{ EVENT_SPONSOR : creates + TALK_PROPOSAL }o--|| BUZZ_EVENT : for + TALK_PROPOSAL ||--o{ EVENT_TALK : maps_to +``` + +## Frontend Page -> API Map +- `dashboard/src/pages/BookTickets.vue` + - `event_manager.api.get_event_booking_data` to load event booking config. + - `event_manager.api.process_booking` via `dashboard/src/components/BookingForm.vue`. +- `dashboard/src/pages/BookingDetails.vue` + - `event_manager.api.get_booking_details` for booking + ticket data. + - `event_manager.api.create_cancellation_request` via `dashboard/src/components/CancellationRequestDialog.vue`. + - `event_manager.api.transfer_ticket` via `dashboard/src/components/TicketsSection.vue`. +- `dashboard/src/pages/TicketDetails.vue` + - `event_manager.api.get_ticket_details`. + - `event_manager.api.change_add_on_preference` via `dashboard/src/components/AddOnPreferenceDialog.vue`. + - `event_manager.api.transfer_ticket` via `dashboard/src/components/TicketTransferDialog.vue`. +- `dashboard/src/pages/SponsorshipsList.vue` + - `event_manager.api.get_user_sponsorship_inquiries`. +- `dashboard/src/pages/SponsorshipDetails.vue` + - `event_manager.api.get_sponsorship_details`. + - `event_manager.api.withdraw_sponsorship_enquiry`. + - `event_manager.api.get_event_payment_gateways` and `event_manager.api.create_sponsorship_payment_link` via `dashboard/src/components/SponsorshipPaymentDialog.vue`. +- `dashboard/src/pages/CheckInScanner.vue` + - `event_manager.api.validate_ticket_for_checkin` and `event_manager.api.checkin_ticket` via `dashboard/src/composables/useTicketValidation.js`. +- `dashboard/src/data/user.js` + - `event_manager.api.get_user_info` for session user details and roles. +- `dashboard/src/composables/useLanguage.js` + - `event_manager.api.get_enabled_languages`, `event_manager.api.update_user_language`. +- `dashboard/src/translation.js` + - `event_manager.api.get_translations`. + +## Feature Development Checklist (Common Changes) +- Tickets / booking changes + - Update `event_manager/api.py` for API shape changes and `Event Booking`/`Event Ticket` doctypes for business logic. + - Keep `BookingForm.vue` payload mapping in sync with backend expectations. + - Update reports if new fields should be exported (`Detailed Event Registrations`). +- Add-on or custom field changes + - Check `Pohodex Event Manager Custom Field` logic and `Additional Field` usage in booking/tickets. + - Update frontend rendering in `CustomFieldInput.vue` and `BookingForm.vue`. +- Payment flow changes + - Update `event_manager/payments.py` and any event-scoped gateway selection logic. + - Ensure payment redirects still land on `/dashboard/...?...success=true`. +- Sponsorship flow changes + - Update `Sponsorship Enquiry` for status transitions and `SponsorshipDetails.vue` for UI state. + - Verify sponsor creation in `on_payment_authorized`. +- Check-in changes + - Update `event_manager.api.validate_ticket_for_checkin` and `CheckInScanner.vue`/`useTicketValidation.js`. + - Confirm role gating for `Frontdesk Manager` remains consistent. +- Event publishing or public pages + - Review `Pohodex Event Manager Event` route generation and `Additional Event Page` route uniqueness. + - If using Builder, update assets under `event_manager/builder_files` and publish to `event_manager/public/builder_files`. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e07d1b1 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,368 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +At the start of every session, read `.claude/memory/memory.md` to load project context. +After completing significant work (new patterns, architectural decisions, solved problems), +update `.claude/memory/memory.md`. Keep it under 300 lines — summarize when it grows. + +--- + +## This defines the best practices to write backend code in the Frappe Framework + +* Frappe Framework is a full-stack web application framework that contains all the necessary components for building modern web applications. +* It provides background workers using Redis, real-time updates using sockets, and a database layer using MariaDB. +* Bench is the official command-line tool for managing Frappe applications. + +## Backend Development + +### JSON & Request Handling + +* Always use built-in functions for parsing JSON: + + * `frappe.parse_json` (handles dicts, lists, and JSON strings safely) + +* Never use `json.loads` directly on request data. + +* For outbound HTTP requests (calling external APIs), use: + + * `frappe.integration.utils.make_get_request` + * `frappe.integration.utils.make_post_request` + * `frappe.integration.utils.make_put_request` + * `frappe.integration.utils.make_patch_request` + +### Datatype Conversion & Utilities + +* For converting datatypes (e.g. str → int, str → float, etc.) use built-in helpers: + + * `frappe.utils.data.cint` + * `frappe.utils.data.cstr` + * `frappe.utils.data.flt` + * `frappe.utils.data.getdate` + * `frappe.utils.data.get_datetime` + +* `frappe.utils.data` contains most conversion and formatting helpers you will ever need: + + * date / datetime parsing + * currency formatting + * number formatting + +* Do NOT create custom utility functions for these conversions. + +* If unsure, ask before implementing. + +### DocType Access Patterns + +* When fetching an existing DocType, prefer: + + * `frappe.get_cached_doc` + +* Use `frappe.get_doc` when: + + * creating a new document + + * To create a new doc go to bench console via bench --site sitename console and use frappe.new_doc("DocType") and then create the doc, don't create the doc via json as the validations doesn't run + + +### Optimization + +* Don't use get_doc or get_cached_doc inside for loop it creates n+1 db problem use frappe.get_all with all the params required and then loop over that list + +### Database Access + +* Prefer ORM methods: + + * `frappe.get_all` + * `frappe.get_list` + * `frappe.db.get_value` + +* Avoid raw SQL absolutely. + + +### Permissions & Security + +* Always respect user permissions. +* Use `ignore_permissions=True` only when absolutely required and justified. + +### Background Jobs & Performance + +* For long-running or heavy operations, always use: + + * `frappe.enqueue` + +* Never block request-response cycles with heavy business logic. + +### Error Handling & Logging + +* Use `frappe.throw` or specific exceptions like `frappe.ValidationError` for user-facing errors. +* Use `frappe.log_error` for unexpected or system-level exceptions. +* Avoid bare `except:` blocks. + +### General Guidelines + +* Prefer framework conventions over custom implementations. +* Keep business logic out of controllers where possible. +* Write readable, predictable, and maintainable code. + + + +## Frontend Development + +1. Always use async/await; avoid callback-based patterns and nested promises. + +2. Use Frappe-provided APIs for server calls: `frappe.call` with `async: true`. Prefer Promise-based usage over callbacks. + +3. Use Frappe's global JS helpers instead of native JS equivalents: + * `cstr()` instead of `String()` + * `cint()` instead of `parseInt()` + * `flt()` instead of `parseFloat()` + * `is_null()` instead of manual null/undefined/empty checks + * `format_currency()` for currency formatting + + +## Crawling + +Always use gemini as much as possible for getting the context, to get the help use gemini --help + +For checking if the site works you can use the agent-browser use agent-browser --help to get the context for it + + +## Commands + +### Frontend (Dashboard) +```bash +# dev server +yarn dev # or: cd dashboard && yarn dev + +# build for production +yarn build # outputs to event_manager/public/dashboard + event_manager/www/dashboard.html + +# lint/format frontend +cd dashboard && yarn lint +``` + +### Backend (Python) + +Always run bench migrate after doctype schema changes. + +```bash +# linting/formatting (via pre-commit) +pre-commit run --all-files + +# run ruff directly +ruff check event_manager/ +ruff format event_manager/ + +# install app to site +bench --site [site-name] install-app event_manager +``` + +Use bench --help to see how to work with frappe bench, e.g. bench execute, bench console, etc. are very useful + +### Testing + +There are unit tests, run using bench run-tests. Site name is event_manager.localhost, but if not found, ask user for it. The credentials are Administrator/admin. + +* To test in UI, use agent-browser. +* For frontend changes use :8080 since yarn dev server is running. +* Use in headed mode unless specified + +## Architecture + +**Three-tier stack:** +1. **Backend**: Frappe Framework (Python) - DocTypes, API, permissions, scheduler +2. **Dashboard**: Vue 3 + FrappeUI + Vite - attendee/sponsor/checkin UI + +**Core entity**: `Pohodex Event Manager Event` DocType drives everything (tickets, sponsors, schedule, payments). + +**Main modules** (inside `event_manager/`): +- `events/` - Event, Venue, Category, Talks, Sponsors, Check-ins +- `ticketing/` - Bookings, Tickets, Add-ons, Cancellations, Coupons +- `proposals/` - Talk Proposals, Sponsorship Enquiries +- `event_manager/` - Settings, Custom Fields +- `api.py` - whitelisted API methods for dashboard +- `payments.py` - integration with frappe/payments app + +**Frontend structure** (inside `dashboard/`): +- `src/pages/` - route components (BookTickets, TicketDetails, CheckInScanner, etc) +- `src/components/` - BookingForm, dialogs, shared UI +- `src/composables/` - reusable logic (useTicketValidation, usePaymentSuccess, etc) +- `src/data/` - frappe-ui resources for API calls +- Vite builds to `event_manager/public/dashboard/`, router base is `/dashboard` + +**Key flows:** +- Booking: load event data → fill form → create booking → generate payment link → on payment auth → submit booking → generate tickets + QR + email +- Ticket actions: transfer, cancel, change add-on (window checks from Pohodex Event Manager Settings) +- Sponsorship: enquiry → approval → payment link → payment auth → create sponsor record +- Check-in: scan QR → validate → create check-in record (requires Frontdesk Manager role) + +**Integrations:** +- `frappe/payments` required for payment gateways +- `buildwithhussain/zoom_integration` optional for webinar creation/registration + +## Key Paths for Common Tasks + +**Booking changes**: `event_manager/api.py`, `event_manager/ticketing/doctype/event_booking/`, `dashboard/src/components/BookingForm.vue` + +**Ticket lifecycle**: `event_manager/ticketing/doctype/event_ticket/`, `dashboard/src/pages/TicketDetails.vue` + +**Sponsorships**: `event_manager/proposals/doctype/sponsorship_enquiry/`, `dashboard/src/pages/SponsorshipDetails.vue` + +**Check-in**: `event_manager/api.py` (validate_ticket_for_checkin, checkin_ticket), `dashboard/src/pages/CheckInScanner.vue` + +**Event config**: `event_manager/events/doctype/buzz_event/` + +**Reports**: `event_manager/events/report/` and `event_manager/ticketing/report/` + + +## Joining or creating report + "Never write `frappe.db.sql` again" +=========================================================== + +1. **Ban `frappe.db.sql` in new code** + * Add a pre-commit rule or CI step that greps for `\.db\.sql` and fails the build. + * Legacy code => wrap in `frappe.db.sql("...", as_dict=1)` and add a `# TODO-QB` comment so the next refactor is trackable. + +2. **Use the typed entry point** + ```python + from frappe.query_builder import DocType, Field + from frappe.query_builder.functions import Count, Sum, Coalesce, Date + ``` + Never `import pypika` directly; the `frappe.qb` namespace already returns the correct `MariaDB/PostgreSQL` dialect. + +3. **Parameterise, never interpolate** + ```python + # Bad + frappe.db.sql(f"... {user_input}") # injection bomb + # Good + frappe.qb.from_(...).where(table.field == user_input) # auto-escaped + ``` + +4. **Prefer joins over N+1** + ```python + so = DocType("Sales Order") + si = DocType("Sales Invoice") + query = ( + frappe.qb.from_(so) + .left_join(si) + .on(so.name == si.sales_order) + .select(so.name, si.name) + .where(so.customer == customer) + ) + ``` + One round-trip, no loops. + +5. **Sub-queries > raw SQL strings** + Need *"latest row per group"*? + ```python + latest = ( + frappe.qb.from_(si) + .select(si.name) + .where(si.sales_order == so.name) + .orderby(si.creation, order=Order.desc) + .limit(1) + ) + query = frappe.qb.from_(so).where(so.name == latest) + ``` + Keeps everything composable and dialect-agnostic. + +6. **Use `case` for conditional aggregates** + ```python + from frappe.query_builder.functions import Case + paid_amt = Sum( + Case() + .when(si.status == "Paid", si.grand_total) + .else_(0) + ) + ``` + +7. **Respect Frappe field casing** + * SQL column: `grand_total` + * Frappe field: `grand_total` + * No back-ticks needed; QB adds the correct quotes per DB. + +8. **Use `as_dict=True` or ORM objects** + ```python + rows = query.run(as_dict=True) # list[dict] + docs = query.run(as_dict=False) # list[tuple] + obj = frappe.get_doc("Doctype", pk) # when you need the full DocType hooks + ``` + +9. **Pagination with `limit_page_length` and `limit_start`** + ```python + query = query.limit(limit_page_length).offset(limit_start) + ``` + Same pattern the REST API uses. + +10. **Index-friendly WHERE order** + Put indexed columns first (`company`, `customer`, `status`) so MariaDB/PostgreSQL can use composite indexes. + +11. **Avoid `SELECT *` in reports** + Explicit list of fields keeps wire-size small and prevents breaking changes when new fields are added. + +13. **Cache heavy aggregations** + ```python + @frappe.whitelist() + @redis_cache(ttl=300) + def get_dashboard_stats(company): + inv = DocType("Sales Invoice") + total = frappe.qb.from_(inv).select(Sum(inv.grand_total)).where(inv.company == company).run() + return total[0][0] or 0 + ``` + + +Quick migration template +---------------------- + +Legacy: +```python +rows = frappe.db.sql(""" + select name, grand_total + from `tabSales Invoice` + where customer = %s + and docstatus = 1 +""", customer, as_dict=1) +``` + +QB equivalent: +```python +si = DocType("Sales Invoice") +rows = ( + frappe.qb.from_(si) + .select(si.name, si.grand_total) + .where((si.customer == customer) & (si.docstatus == 1)) + .run(as_dict=True) +) +``` + + +### Report Patterns +- Entry: `def execute(filters): return get_columns(), get_data(filters)` +- QB imports: `from frappe.query_builder import DocType` + `functions.Sum, Case, Count` +- Build lookup maps first, then loop + merge (avoid N+1) +- Caching: `@redis_cache(ttl=seconds)` for conversion factors + +### Token-Saving Workflow +- Default to `/model haiku` for routine edits, `/model sonnet` for moderate tasks +- Use `/model opus` ONLY for architecture, debugging complex issues +- Use `/compact` after completing each subtask +- Use `gemini -p "prompt"` via stdin to read/summarize files without burning Claude tokens +- Scope tasks narrowly: one feature/fix per session +- Dump progress to `.claude/memory/scratch.md` before session ends +- At session START: read `.claude/memory/scratch.md` — if it has content, resume from there +- At session END (or when user says "dump progress"): fill in scratch.md with current task state +- After resuming from scratch.md, clear it once the task is complete + + +### Variable and Function naming convention + +- always use full names for variables don't use abbreviations for ex: use +"for row in rows" instead of "for r in rows" +proxy_sku = DocType("Proxy SKU") instead of ps = DocType("Proxy SKU") +- avoid starting python functions with underscore "_" unless it's a private version behind a whitelisted function (e.g. `get_dial_codes` (whitelisted) -> `_get_dial_codes` (cached logic)) +- use camelCase in JS and follow the surrounding code style in the project +- always put imports at the top of the file, never inside functions + +## Notes + +- Read `ARCHITECTURE.md` for comprehensive details on data model, API surface, flows diff --git a/README.md b/README.md index 41812cb..3c2e944 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,102 @@ -# event-manager -Event manager pro Frappe \ No newline at end of file + +
+ CI + UI Tests + GitHub stars + License: AGPL v3 + GitHub commit activity +
+ +![Pohodex Event Manager Event DocType](.github/images/fe-event-main-form.png) + +Open Source, Powerful, and Comprehensive Event Management Platform + +### Learn & Connect +[Telegram Public Group](https://t.me/bwh_buzz) +### Stack / Architecture + +1. Frappe Framework: The Backend and Admin Interface +2. FrappeUI (based on Vue & TailwindCSS): For the frontend dashboard (for attendee, sponsors, etc.) +3. Frappe Builder: For the public pages like events list and details page. + +### The Main Entity + +The **Pohodex Event Manager Event** DocType/Form is the primary entity of the system. Once you have created an event, you can setup ticket types, sponsorship tiers, add-ons (like T-Shirts, Meals, etc.), schedule, and much more! + +### Features + +This is not an exhaustive list by any means, just to give you an idea 😃 + +#### Dynamic Ticket & Add-on Types + +![Dynamic Ticket Types and Add-ons](.github/images/ticket-types-and-add-ons.png) + +#### The Booking Form + +Once you have defined the proper ticket types, add-ons, and publish your event, the booking form will dynamically use it for booking. + +![Booking Form](.github/images/booking-form.png) + +#### Payments App Integration + +This app depends on Frappe's Payments app for online payments. You can select a Payment Gateway in the event form. BTW GST collection is just a check-box away 😉 + +#### The Dashboard + +![Booking Details Page](.github/images/booking-details-page.png) + +#### Ticket Management + +The benefits of having a "self-service" dashboard for attendees is that they can modify their bookings on their own (the deadlines can be configured from the **Pohodex Event Manager Settings**). For example, changing their T-Shirt Size after booking: + +![Change Add-on Preference](.github/images/ticket-updates.png) + +They can also transfer tickets or request for cancellation. + +#### Sponsorship Management + +Folks can enquire about sponsoring an event and upon approval from the event management team (from desk), they can directly pay from the dashboard too: + +![Sponsorship Payment](.github/images/sponsorship-payment.png) + +*As soon as they pay, their logo appears on the event page!* + +![Sponsorship Management](.github/images/sponsorship-management.png) + +### Installation + +You can install this app using the [bench](https://github.com/frappe/bench) CLI: + +```bash +cd $PATH_TO_YOUR_BENCH +bench get-app BuildWithHussain/event_manager --branch main +bench install-app event_manager +``` + +### Contributing + +This app uses `pre-commit` for code formatting and linting. Please [install pre-commit](https://pre-commit.com/#installation) and enable it for this repository: + +```bash +cd apps/event_manager +pre-commit install +``` + +Pre-commit is configured to use the following tools for checking and formatting your code: + +- ruff +- eslint +- prettier +- pyupgrade +### CI + +This app can use GitHub Actions for CI. The following workflows are configured: + +- CI: Installs this app and runs unit tests on every push to `develop` branch. +- Linters: Runs [Frappe Semgrep Rules](https://github.com/frappe/semgrep-rules) and [pip-audit](https://pypi.org/project/pip-audit/) on every pull request. + + +### License + +agpl-3.0 diff --git a/dashboard/.gitignore b/dashboard/.gitignore new file mode 100644 index 0000000..53f7466 --- /dev/null +++ b/dashboard/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local \ No newline at end of file diff --git a/dashboard/README.md b/dashboard/README.md new file mode 100644 index 0000000..a646151 --- /dev/null +++ b/dashboard/README.md @@ -0,0 +1,50 @@ +# Frappe UI Starter + +This template should help get you started developing custom frontend for Frappe +apps with Vue 3 and the Frappe UI package. + +![Auth](https://user-images.githubusercontent.com/34810212/236846289-ac31c292-81ea-4456-be65-95773a4049be.png) + +![Home](https://user-images.githubusercontent.com/34810212/236846299-fd534e2b-1c06-4f01-a4f2-91a27547cd55.png) + +This boilerplate sets up Vue 3, Vue Router, TailwindCSS, and Frappe UI out of +the box. It also has basic authentication frontend. + +## Docs + +[Frappe UI Website](https://frappeui.com) + +## Usage + +This template is meant to be cloned inside an existing Frappe App. Assuming your +apps name is `todo`. Clone this template in the root folder of your app using `degit`. + +``` +cd apps/todo +npx degit NagariaHussain/doppio_frappeui_starter frontend +cd frontend +yarn +yarn dev +``` + +In a development environment, you need to put the below key-value pair in your `site_config.json` file: + +``` +"ignore_csrf": 1 +``` + +This will prevent `CSRFToken` errors while using the vite dev server. In production environment, the `csrf_token` is attached to the `window` object in `index.html` for you. + +The Vite dev server will start on the port `8080`. This can be changed from `vite.config.js`. +The development server is configured to proxy your frappe app (usually running on port `8000`). If you have a site named `todo.test`, open `http://todo.test:8080` in your browser. If you see a button named "Click to send 'ping' request", congratulations! + +If you notice the browser URL is `/frontend`, this is the base URL where your frontend app will run in production. +To change this, open `src/router.js` and change the base URL passed to `createWebHistory`. + +## Resources + +- [Vue 3](https://v3.vuejs.org/guide/introduction.html) +- [Vue Router](https://next.router.vuejs.org/guide/) +- [Frappe UI](https://github.com/frappe/frappe-ui) +- [TailwindCSS](https://tailwindcss.com/docs/utility-first) +- [Vite](https://vitejs.dev/guide/) diff --git a/dashboard/auto-imports.d.ts b/dashboard/auto-imports.d.ts new file mode 100644 index 0000000..b72c4a7 --- /dev/null +++ b/dashboard/auto-imports.d.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// noinspection JSUnusedGlobalSymbols +// Generated by unplugin-auto-import +// biome-ignore lint: disable +export {} +declare global { + const LucideEdit: typeof import("~icons/lucide/edit")["default"] + const LucideMic: typeof import("~icons/lucide/mic")["default"] + const LucideRadio: typeof import("~icons/lucide/radio")["default"] + const LucideSettings: typeof import("~icons/lucide/settings")["default"] + const LucideUserPen: typeof import("~icons/lucide/user-pen")["default"] +} diff --git a/dashboard/biome.json b/dashboard/biome.json new file mode 100644 index 0000000..bde8525 --- /dev/null +++ b/dashboard/biome.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "ignore": [] + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "semicolons": "asNeeded" + } + } +} diff --git a/dashboard/components.d.ts b/dashboard/components.d.ts new file mode 100644 index 0000000..308f757 --- /dev/null +++ b/dashboard/components.d.ts @@ -0,0 +1,52 @@ +/* eslint-disable */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +// biome-ignore lint: disable +export {} + +/* prettier-ignore */ +declare module 'vue' { + export interface GlobalComponents { + AddOnPreferenceDialog: typeof import('./src/components/AddOnPreferenceDialog.vue')['default'] + AttendeeFormControl: typeof import('./src/components/AttendeeFormControl.vue')['default'] + BackButton: typeof import('./src/components/common/BackButton.vue')['default'] + BaseCustomEventForm: typeof import('./src/components/BaseCustomEventForm.vue')['default'] + BillingDetails: typeof import('./src/components/BillingDetails.vue')['default'] + BookingEventInfo: typeof import('./src/components/BookingEventInfo.vue')['default'] + BookingFinancialSummary: typeof import('./src/components/BookingFinancialSummary.vue')['default'] + BookingForm: typeof import('./src/components/BookingForm.vue')['default'] + BookingHeader: typeof import('./src/components/BookingHeader.vue')['default'] + BookingSummary: typeof import('./src/components/BookingSummary.vue')['default'] + BuzzLogo: typeof import('./src/components/common/BuzzLogo.vue')['default'] + CancellationRequestDialog: typeof import('./src/components/CancellationRequestDialog.vue')['default'] + CancellationRequestNotice: typeof import('./src/components/CancellationRequestNotice.vue')['default'] + CustomFieldInput: typeof import('./src/components/CustomFieldInput.vue')['default'] + CustomFieldsSection: typeof import('./src/components/CustomFieldsSection.vue')['default'] + EventDetailsHeader: typeof import('./src/components/EventDetailsHeader.vue')['default'] + EventSelector: typeof import('./src/components/EventSelector.vue')['default'] + EventSponsorForm: typeof import('./src/components/EventSponsorForm.vue')['default'] + LanguageSwitcher: typeof import('./src/components/LanguageSwitcher.vue')['default'] + LoginDialog: typeof import('./src/components/LoginDialog.vue')['default'] + LoginRequired: typeof import('./src/components/LoginRequired.vue')['default'] + Navbar: typeof import('./src/components/Navbar.vue')['default'] + OfflinePaymentDialog: typeof import('./src/components/OfflinePaymentDialog.vue')['default'] + PaymentGatewayDialog: typeof import('./src/components/PaymentGatewayDialog.vue')['default'] + PhoneInput: typeof import('./src/components/PhoneInput.vue')['default'] + ProfileView: typeof import('./src/components/ProfileView.vue')['default'] + ProposalEditDialog: typeof import('./src/components/ProposalEditDialog.vue')['default'] + QRCodeExpandDialog: typeof import('./src/components/QRCodeExpandDialog.vue')['default'] + QRScanner: typeof import('./src/components/QRScanner.vue')['default'] + RestrictionNotices: typeof import('./src/components/RestrictionNotices.vue')['default'] + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + SponsorLogoUploader: typeof import('./src/components/SponsorLogoUploader.vue')['default'] + SponsorshipPaymentDialog: typeof import('./src/components/SponsorshipPaymentDialog.vue')['default'] + SuccessMessage: typeof import('./src/components/SuccessMessage.vue')['default'] + TicketCard: typeof import('./src/components/TicketCard.vue')['default'] + TicketDetailsModal: typeof import('./src/components/TicketDetailsModal.vue')['default'] + TicketsSection: typeof import('./src/components/TicketsSection.vue')['default'] + TicketTransferDialog: typeof import('./src/components/TicketTransferDialog.vue')['default'] + TransferTicketDialog: typeof import('./src/components/TransferTicketDialog.vue')['default'] + } +} diff --git a/dashboard/index.html b/dashboard/index.html new file mode 100644 index 0000000..4c430fb --- /dev/null +++ b/dashboard/index.html @@ -0,0 +1,13 @@ + + + + + + + Pohodex Event Manager Dashboard + + +
+ + + diff --git a/dashboard/package.json b/dashboard/package.json new file mode 100644 index 0000000..c3955d4 --- /dev/null +++ b/dashboard/package.json @@ -0,0 +1,38 @@ +{ + "name": "frappe-ui-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build --base=/assets/event_manager/dashboard/ && yarn copy-html-entry", + "preview": "vite preview", + "lint": "biome check --write .", + "typecheck": "./typecheck.sh", + "copy-html-entry": "cp ../event_manager/public/dashboard/index.html ../event_manager/www/dashboard.html" + }, + "dependencies": { + "@vueuse/core": "^13.6.0", + "@vueuse/router": "^13.6.0", + "canvas-confetti": "^1.9.3", + "feather-icons": "^4.29.2", + "frappe-ui": "^0.1.257", + "socket.io-client": "^4.7.2", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@types/canvas-confetti": "^1.9.0", + "@types/node": "^25.2.0", + "@vitejs/plugin-vue": "^5.1.4", + "autoprefixer": "^10.4.2", + "html5-qrcode": "^2.3.8", + "postcss": "^8.4.5", + "tailwindcss": "^3.4.15", + "typescript": "^5.9.3", + "unplugin-auto-import": "0.18.6", + "vite": "^5.4.10", + "vue-tsc": "^3.2.4" + } +} diff --git a/dashboard/postcss.config.js b/dashboard/postcss.config.js new file mode 100644 index 0000000..7b75c83 --- /dev/null +++ b/dashboard/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/dashboard/public/favicon.png b/dashboard/public/favicon.png new file mode 100644 index 0000000..b51db82 Binary files /dev/null and b/dashboard/public/favicon.png differ diff --git a/dashboard/src/App.vue b/dashboard/src/App.vue new file mode 100644 index 0000000..6987cad --- /dev/null +++ b/dashboard/src/App.vue @@ -0,0 +1,17 @@ + + + diff --git a/dashboard/src/assets/Inter/Inter-Black.woff b/dashboard/src/assets/Inter/Inter-Black.woff new file mode 100644 index 0000000..c7737ed Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-Black.woff differ diff --git a/dashboard/src/assets/Inter/Inter-Black.woff2 b/dashboard/src/assets/Inter/Inter-Black.woff2 new file mode 100644 index 0000000..b16b995 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-Black.woff2 differ diff --git a/dashboard/src/assets/Inter/Inter-BlackItalic.woff b/dashboard/src/assets/Inter/Inter-BlackItalic.woff new file mode 100644 index 0000000..b5f1447 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-BlackItalic.woff differ diff --git a/dashboard/src/assets/Inter/Inter-BlackItalic.woff2 b/dashboard/src/assets/Inter/Inter-BlackItalic.woff2 new file mode 100644 index 0000000..a3f1b70 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-BlackItalic.woff2 differ diff --git a/dashboard/src/assets/Inter/Inter-Bold.woff b/dashboard/src/assets/Inter/Inter-Bold.woff new file mode 100644 index 0000000..e384555 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-Bold.woff differ diff --git a/dashboard/src/assets/Inter/Inter-Bold.woff2 b/dashboard/src/assets/Inter/Inter-Bold.woff2 new file mode 100644 index 0000000..835dd49 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-Bold.woff2 differ diff --git a/dashboard/src/assets/Inter/Inter-BoldItalic.woff b/dashboard/src/assets/Inter/Inter-BoldItalic.woff new file mode 100644 index 0000000..ffac3f5 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-BoldItalic.woff differ diff --git a/dashboard/src/assets/Inter/Inter-BoldItalic.woff2 b/dashboard/src/assets/Inter/Inter-BoldItalic.woff2 new file mode 100644 index 0000000..1a41a14 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-BoldItalic.woff2 differ diff --git a/dashboard/src/assets/Inter/Inter-ExtraBold.woff b/dashboard/src/assets/Inter/Inter-ExtraBold.woff new file mode 100644 index 0000000..885ac94 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-ExtraBold.woff differ diff --git a/dashboard/src/assets/Inter/Inter-ExtraBold.woff2 b/dashboard/src/assets/Inter/Inter-ExtraBold.woff2 new file mode 100644 index 0000000..ae956b1 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-ExtraBold.woff2 differ diff --git a/dashboard/src/assets/Inter/Inter-ExtraBoldItalic.woff b/dashboard/src/assets/Inter/Inter-ExtraBoldItalic.woff new file mode 100644 index 0000000..d6cf862 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-ExtraBoldItalic.woff differ diff --git a/dashboard/src/assets/Inter/Inter-ExtraBoldItalic.woff2 b/dashboard/src/assets/Inter/Inter-ExtraBoldItalic.woff2 new file mode 100644 index 0000000..8657899 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-ExtraBoldItalic.woff2 differ diff --git a/dashboard/src/assets/Inter/Inter-ExtraLight.woff b/dashboard/src/assets/Inter/Inter-ExtraLight.woff new file mode 100644 index 0000000..ff76919 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-ExtraLight.woff differ diff --git a/dashboard/src/assets/Inter/Inter-ExtraLight.woff2 b/dashboard/src/assets/Inter/Inter-ExtraLight.woff2 new file mode 100644 index 0000000..694b2df Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-ExtraLight.woff2 differ diff --git a/dashboard/src/assets/Inter/Inter-ExtraLightItalic.woff b/dashboard/src/assets/Inter/Inter-ExtraLightItalic.woff new file mode 100644 index 0000000..c6ed13a Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-ExtraLightItalic.woff differ diff --git a/dashboard/src/assets/Inter/Inter-ExtraLightItalic.woff2 b/dashboard/src/assets/Inter/Inter-ExtraLightItalic.woff2 new file mode 100644 index 0000000..9a7bd11 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-ExtraLightItalic.woff2 differ diff --git a/dashboard/src/assets/Inter/Inter-Italic.woff b/dashboard/src/assets/Inter/Inter-Italic.woff new file mode 100644 index 0000000..4fdb59d Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-Italic.woff differ diff --git a/dashboard/src/assets/Inter/Inter-Italic.woff2 b/dashboard/src/assets/Inter/Inter-Italic.woff2 new file mode 100644 index 0000000..deca637 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-Italic.woff2 differ diff --git a/dashboard/src/assets/Inter/Inter-Light.woff b/dashboard/src/assets/Inter/Inter-Light.woff new file mode 100644 index 0000000..42850ac Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-Light.woff differ diff --git a/dashboard/src/assets/Inter/Inter-Light.woff2 b/dashboard/src/assets/Inter/Inter-Light.woff2 new file mode 100644 index 0000000..65a7dad Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-Light.woff2 differ diff --git a/dashboard/src/assets/Inter/Inter-LightItalic.woff b/dashboard/src/assets/Inter/Inter-LightItalic.woff new file mode 100644 index 0000000..c4ed9a9 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-LightItalic.woff differ diff --git a/dashboard/src/assets/Inter/Inter-LightItalic.woff2 b/dashboard/src/assets/Inter/Inter-LightItalic.woff2 new file mode 100644 index 0000000..555fc55 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-LightItalic.woff2 differ diff --git a/dashboard/src/assets/Inter/Inter-Medium.woff b/dashboard/src/assets/Inter/Inter-Medium.woff new file mode 100644 index 0000000..495faef Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-Medium.woff differ diff --git a/dashboard/src/assets/Inter/Inter-Medium.woff2 b/dashboard/src/assets/Inter/Inter-Medium.woff2 new file mode 100644 index 0000000..871ce4c Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-Medium.woff2 differ diff --git a/dashboard/src/assets/Inter/Inter-MediumItalic.woff b/dashboard/src/assets/Inter/Inter-MediumItalic.woff new file mode 100644 index 0000000..389c7a2 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-MediumItalic.woff differ diff --git a/dashboard/src/assets/Inter/Inter-MediumItalic.woff2 b/dashboard/src/assets/Inter/Inter-MediumItalic.woff2 new file mode 100644 index 0000000..aa80579 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-MediumItalic.woff2 differ diff --git a/dashboard/src/assets/Inter/Inter-Regular.woff b/dashboard/src/assets/Inter/Inter-Regular.woff new file mode 100644 index 0000000..fa7715d Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-Regular.woff differ diff --git a/dashboard/src/assets/Inter/Inter-Regular.woff2 b/dashboard/src/assets/Inter/Inter-Regular.woff2 new file mode 100644 index 0000000..b52dd0a Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-Regular.woff2 differ diff --git a/dashboard/src/assets/Inter/Inter-SemiBold.woff b/dashboard/src/assets/Inter/Inter-SemiBold.woff new file mode 100644 index 0000000..18d7749 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-SemiBold.woff differ diff --git a/dashboard/src/assets/Inter/Inter-SemiBold.woff2 b/dashboard/src/assets/Inter/Inter-SemiBold.woff2 new file mode 100644 index 0000000..ece5204 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-SemiBold.woff2 differ diff --git a/dashboard/src/assets/Inter/Inter-SemiBoldItalic.woff b/dashboard/src/assets/Inter/Inter-SemiBoldItalic.woff new file mode 100644 index 0000000..8ee6439 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-SemiBoldItalic.woff differ diff --git a/dashboard/src/assets/Inter/Inter-SemiBoldItalic.woff2 b/dashboard/src/assets/Inter/Inter-SemiBoldItalic.woff2 new file mode 100644 index 0000000..b32c0ba Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-SemiBoldItalic.woff2 differ diff --git a/dashboard/src/assets/Inter/Inter-Thin.woff b/dashboard/src/assets/Inter/Inter-Thin.woff new file mode 100644 index 0000000..1a22286 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-Thin.woff differ diff --git a/dashboard/src/assets/Inter/Inter-Thin.woff2 b/dashboard/src/assets/Inter/Inter-Thin.woff2 new file mode 100644 index 0000000..c56bc7c Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-Thin.woff2 differ diff --git a/dashboard/src/assets/Inter/Inter-ThinItalic.woff b/dashboard/src/assets/Inter/Inter-ThinItalic.woff new file mode 100644 index 0000000..d8ec837 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-ThinItalic.woff differ diff --git a/dashboard/src/assets/Inter/Inter-ThinItalic.woff2 b/dashboard/src/assets/Inter/Inter-ThinItalic.woff2 new file mode 100644 index 0000000..eca5608 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-ThinItalic.woff2 differ diff --git a/dashboard/src/assets/Inter/Inter-italic.var.woff2 b/dashboard/src/assets/Inter/Inter-italic.var.woff2 new file mode 100644 index 0000000..1f5d926 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-italic.var.woff2 differ diff --git a/dashboard/src/assets/Inter/Inter-roman.var.woff2 b/dashboard/src/assets/Inter/Inter-roman.var.woff2 new file mode 100644 index 0000000..05621d8 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter-roman.var.woff2 differ diff --git a/dashboard/src/assets/Inter/Inter.var.woff2 b/dashboard/src/assets/Inter/Inter.var.woff2 new file mode 100644 index 0000000..46bb515 Binary files /dev/null and b/dashboard/src/assets/Inter/Inter.var.woff2 differ diff --git a/dashboard/src/assets/Inter/inter.css b/dashboard/src/assets/Inter/inter.css new file mode 100644 index 0000000..9f8f80f --- /dev/null +++ b/dashboard/src/assets/Inter/inter.css @@ -0,0 +1,152 @@ +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 100; + font-display: swap; + src: url("Inter-Thin.woff2?v=3.12") format("woff2"), + url("Inter-Thin.woff?v=3.12") format("woff"); +} +@font-face { + font-family: "Inter"; + font-style: italic; + font-weight: 100; + font-display: swap; + src: url("Inter-ThinItalic.woff2?v=3.12") format("woff2"), + url("Inter-ThinItalic.woff?v=3.12") format("woff"); +} + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 200; + font-display: swap; + src: url("Inter-ExtraLight.woff2?v=3.12") format("woff2"), + url("Inter-ExtraLight.woff?v=3.12") format("woff"); +} +@font-face { + font-family: "Inter"; + font-style: italic; + font-weight: 200; + font-display: swap; + src: url("Inter-ExtraLightItalic.woff2?v=3.12") format("woff2"), + url("Inter-ExtraLightItalic.woff?v=3.12") format("woff"); +} + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 300; + font-display: swap; + src: url("Inter-Light.woff2?v=3.12") format("woff2"), + url("Inter-Light.woff?v=3.12") format("woff"); +} +@font-face { + font-family: "Inter"; + font-style: italic; + font-weight: 300; + font-display: swap; + src: url("Inter-LightItalic.woff2?v=3.12") format("woff2"), + url("Inter-LightItalic.woff?v=3.12") format("woff"); +} + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("Inter-Regular.woff2?v=3.12") format("woff2"), + url("Inter-Regular.woff?v=3.12") format("woff"); +} +@font-face { + font-family: "Inter"; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url("Inter-Italic.woff2?v=3.12") format("woff2"), + url("Inter-Italic.woff?v=3.12") format("woff"); +} + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url("Inter-Medium.woff2?v=3.12") format("woff2"), + url("Inter-Medium.woff?v=3.12") format("woff"); +} +@font-face { + font-family: "Inter"; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url("Inter-MediumItalic.woff2?v=3.12") format("woff2"), + url("Inter-MediumItalic.woff?v=3.12") format("woff"); +} + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url("Inter-SemiBold.woff2?v=3.12") format("woff2"), + url("Inter-SemiBold.woff?v=3.12") format("woff"); +} +@font-face { + font-family: "Inter"; + font-style: italic; + font-weight: 600; + font-display: swap; + src: url("Inter-SemiBoldItalic.woff2?v=3.12") format("woff2"), + url("Inter-SemiBoldItalic.woff?v=3.12") format("woff"); +} + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url("Inter-Bold.woff2?v=3.12") format("woff2"), + url("Inter-Bold.woff?v=3.12") format("woff"); +} +@font-face { + font-family: "Inter"; + font-style: italic; + font-weight: 700; + font-display: swap; + src: url("Inter-BoldItalic.woff2?v=3.12") format("woff2"), + url("Inter-BoldItalic.woff?v=3.12") format("woff"); +} + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 800; + font-display: swap; + src: url("Inter-ExtraBold.woff2?v=3.12") format("woff2"), + url("Inter-ExtraBold.woff?v=3.12") format("woff"); +} +@font-face { + font-family: "Inter"; + font-style: italic; + font-weight: 800; + font-display: swap; + src: url("Inter-ExtraBoldItalic.woff2?v=3.12") format("woff2"), + url("Inter-ExtraBoldItalic.woff?v=3.12") format("woff"); +} + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 900; + font-display: swap; + src: url("Inter-Black.woff2?v=3.12") format("woff2"), + url("Inter-Black.woff?v=3.12") format("woff"); +} +@font-face { + font-family: "Inter"; + font-style: italic; + font-weight: 900; + font-display: swap; + src: url("Inter-BlackItalic.woff2?v=3.12") format("woff2"), + url("Inter-BlackItalic.woff?v=3.12") format("woff"); +} diff --git a/dashboard/src/assets/audio/beep-fail.wav b/dashboard/src/assets/audio/beep-fail.wav new file mode 100644 index 0000000..4790122 Binary files /dev/null and b/dashboard/src/assets/audio/beep-fail.wav differ diff --git a/dashboard/src/assets/audio/beep.wav b/dashboard/src/assets/audio/beep.wav new file mode 100644 index 0000000..5649b0a Binary files /dev/null and b/dashboard/src/assets/audio/beep.wav differ diff --git a/dashboard/src/components/AddOnPreferenceDialog.vue b/dashboard/src/components/AddOnPreferenceDialog.vue new file mode 100644 index 0000000..482c3fd --- /dev/null +++ b/dashboard/src/components/AddOnPreferenceDialog.vue @@ -0,0 +1,152 @@ + + + diff --git a/dashboard/src/components/AttendeeFormControl.vue b/dashboard/src/components/AttendeeFormControl.vue new file mode 100644 index 0000000..4a8226e --- /dev/null +++ b/dashboard/src/components/AttendeeFormControl.vue @@ -0,0 +1,216 @@ + + + + diff --git a/dashboard/src/components/BaseCustomEventForm.vue b/dashboard/src/components/BaseCustomEventForm.vue new file mode 100644 index 0000000..8d04132 --- /dev/null +++ b/dashboard/src/components/BaseCustomEventForm.vue @@ -0,0 +1,322 @@ + + + diff --git a/dashboard/src/components/BillingDetails.vue b/dashboard/src/components/BillingDetails.vue new file mode 100644 index 0000000..c91bc6c --- /dev/null +++ b/dashboard/src/components/BillingDetails.vue @@ -0,0 +1,67 @@ +