commit c405c68a7d64fca23fac2a425c2a745f1282a716 Author: 24c02 <163450896+24c02@users.noreply.github.com> Date: Sat May 31 23:25:41 2025 -0400 INITIAL GOSH DANG COMMIT :3333 diff --git a/.annotaterb.yml b/.annotaterb.yml new file mode 100644 index 0000000..2811be7 --- /dev/null +++ b/.annotaterb.yml @@ -0,0 +1,58 @@ +--- +:position: before +:position_in_additional_file_patterns: before +:position_in_class: before +:position_in_factory: before +:position_in_fixture: before +:position_in_routes: before +:position_in_serializer: before +:position_in_test: before +:classified_sort: true +:exclude_controllers: true +:exclude_factories: false +:exclude_fixtures: false +:exclude_helpers: true +:exclude_scaffolds: true +:exclude_serializers: false +:exclude_sti_subclasses: false +:exclude_tests: false +:force: false +:format_markdown: false +:format_rdoc: false +:format_yard: false +:frozen: false +:ignore_model_sub_dir: false +:ignore_unknown_models: false +:include_version: false +:show_check_constraints: false +:show_complete_foreign_keys: false +:show_foreign_keys: true +:show_indexes: true +:simple_indexes: false +:sort: false +:timestamp: false +:trace: false +:with_comment: true +:with_column_comments: true +:with_table_comments: true +:active_admin: false +:command: +:debug: false +:hide_default_column_types: '' +:hide_limit_column_types: '' +:ignore_columns: +:ignore_routes: +:models: true +:routes: false +:skip_on_db_migrate: false +:target_action: :do_annotations +:wrapper: +:wrapper_close: +:wrapper_open: +:classes_default_to_s: [] +:additional_file_patterns: [] +:model_dir: +- app/models +:require: [] +:root_dir: +- '' diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..325bfc0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,51 @@ +# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. + +# Ignore git directory. +/.git/ +/.gitignore + +# Ignore bundler config. +/.bundle + +# Ignore all environment files. +/.env* + +# Ignore all default key files. +/config/master.key +/config/credentials/*.key + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/.keep + +# Ignore assets. +/node_modules/ +/app/assets/builds/* +!/app/assets/builds/.keep +/public/assets + +# Ignore CI service files. +/.github + +# Ignore Kamal files. +/config/deploy*.yml +/.kamal + +# Ignore development files +/.devcontainer + +# Ignore Docker-related files +/.dockerignore +/Dockerfile* diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8dc4323 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# See https://git-scm.com/docs/gitattributes for more about git attribute files. + +# Mark the database schema as having been generated. +db/schema.rb linguist-generated + +# Mark any vendored files as having been vendored. +vendor/* linguist-vendored +config/credentials/*.yml.enc diff=rails_credentials +config/credentials.yml.enc diff=rails_credentials diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f0527e6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: +- package-ecosystem: bundler + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..186e6b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# Temporary files generated by your text editor or operating system +# belong in git's global ignore instead: +# `$XDG_CONFIG_HOME/git/ignore` or `~/.config/git/ignore` + +# Ignore bundler config. +/.bundle + +# Ignore all environment files. +/.env* + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/ +!/tmp/storage/.keep + +/public/assets + +# Ignore master key for decrypting credentials and more. +/config/master.key + +/app/assets/builds/* +!/app/assets/builds/.keep + +/node_modules + +/config/credentials/production.key + +**/.DS_Store + +# Vite Ruby +/public/vite* +node_modules +# Vite uses dotenv and suggests to ignore local-only env files. See +# https://vitejs.dev/guide/env-and-mode.html#env-files +*.local + +*.key + +app/frontend/images/template_previews/* \ No newline at end of file diff --git a/.kamal/hooks/docker-setup.sample b/.kamal/hooks/docker-setup.sample new file mode 100755 index 0000000..2fb07d7 --- /dev/null +++ b/.kamal/hooks/docker-setup.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Docker set up on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/post-app-boot.sample b/.kamal/hooks/post-app-boot.sample new file mode 100755 index 0000000..70f9c4b --- /dev/null +++ b/.kamal/hooks/post-app-boot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/post-deploy.sample b/.kamal/hooks/post-deploy.sample new file mode 100755 index 0000000..75efafc --- /dev/null +++ b/.kamal/hooks/post-deploy.sample @@ -0,0 +1,14 @@ +#!/bin/sh + +# A sample post-deploy hook +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds" diff --git a/.kamal/hooks/post-proxy-reboot.sample b/.kamal/hooks/post-proxy-reboot.sample new file mode 100755 index 0000000..1435a67 --- /dev/null +++ b/.kamal/hooks/post-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooted kamal-proxy on $KAMAL_HOSTS" diff --git a/.kamal/hooks/pre-app-boot.sample b/.kamal/hooks/pre-app-boot.sample new file mode 100755 index 0000000..45f7355 --- /dev/null +++ b/.kamal/hooks/pre-app-boot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/pre-build.sample b/.kamal/hooks/pre-build.sample new file mode 100755 index 0000000..f87d811 --- /dev/null +++ b/.kamal/hooks/pre-build.sample @@ -0,0 +1,51 @@ +#!/bin/sh + +# A sample pre-build hook +# +# Checks: +# 1. We have a clean checkout +# 2. A remote is configured +# 3. The branch has been pushed to the remote +# 4. The version we are deploying matches the remote +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) + +if [ -n "$(git status --porcelain)" ]; then + echo "Git checkout is not clean, aborting..." >&2 + git status --porcelain >&2 + exit 1 +fi + +first_remote=$(git remote) + +if [ -z "$first_remote" ]; then + echo "No git remote set, aborting..." >&2 + exit 1 +fi + +current_branch=$(git branch --show-current) + +if [ -z "$current_branch" ]; then + echo "Not on a git branch, aborting..." >&2 + exit 1 +fi + +remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1) + +if [ -z "$remote_head" ]; then + echo "Branch not pushed to remote, aborting..." >&2 + exit 1 +fi + +if [ "$KAMAL_VERSION" != "$remote_head" ]; then + echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2 + exit 1 +fi + +exit 0 diff --git a/.kamal/hooks/pre-connect.sample b/.kamal/hooks/pre-connect.sample new file mode 100755 index 0000000..18e61d7 --- /dev/null +++ b/.kamal/hooks/pre-connect.sample @@ -0,0 +1,47 @@ +#!/usr/bin/env ruby + +# A sample pre-connect check +# +# Warms DNS before connecting to hosts in parallel +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +hosts = ENV["KAMAL_HOSTS"].split(",") +results = nil +max = 3 + +elapsed = Benchmark.realtime do + results = hosts.map do |host| + Thread.new do + tries = 1 + + begin + Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) + rescue SocketError + if tries < max + puts "Retrying DNS warmup: #{host}" + tries += 1 + sleep rand + retry + else + puts "DNS warmup failed: #{host}" + host + end + end + + tries + end + end.map(&:value) +end + +retries = results.sum - hosts.size +nopes = results.count { |r| r == max } + +puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ] diff --git a/.kamal/hooks/pre-deploy.sample b/.kamal/hooks/pre-deploy.sample new file mode 100755 index 0000000..1b280c7 --- /dev/null +++ b/.kamal/hooks/pre-deploy.sample @@ -0,0 +1,109 @@ +#!/usr/bin/env ruby + +# A sample pre-deploy hook +# +# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds. +# +# Fails unless the combined status is "success" +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_COMMAND +# KAMAL_SUBCOMMAND +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) + +# Only check the build status for production deployments +if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production" + exit 0 +end + +require "bundler/inline" + +# true = install gems so this is fast on repeat invocations +gemfile(true, quiet: true) do + source "https://rubygems.org" + + gem "octokit" + gem "faraday-retry" +end + +MAX_ATTEMPTS = 72 +ATTEMPTS_GAP = 10 + +def exit_with_error(message) + $stderr.puts message + exit 1 +end + +class GithubStatusChecks + attr_reader :remote_url, :git_sha, :github_client, :combined_status + + def initialize + @remote_url = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/") + @git_sha = `git rev-parse HEAD`.strip + @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"]) + refresh! + end + + def refresh! + @combined_status = github_client.combined_status(remote_url, git_sha) + end + + def state + combined_status[:state] + end + + def first_status_url + first_status = combined_status[:statuses].find { |status| status[:state] == state } + first_status && first_status[:target_url] + end + + def complete_count + combined_status[:statuses].count { |status| status[:state] != "pending"} + end + + def total_count + combined_status[:statuses].count + end + + def current_status + if total_count > 0 + "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..." + else + "Build not started..." + end + end +end + + +$stdout.sync = true + +puts "Checking build status..." +attempts = 0 +checks = GithubStatusChecks.new + +begin + loop do + case checks.state + when "success" + puts "Checks passed, see #{checks.first_status_url}" + exit 0 + when "failure" + exit_with_error "Checks failed, see #{checks.first_status_url}" + when "pending" + attempts += 1 + end + + exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS + + puts checks.current_status + sleep(ATTEMPTS_GAP) + checks.refresh! + end +rescue Octokit::NotFound + exit_with_error "Build status could not be found" +end diff --git a/.kamal/hooks/pre-proxy-reboot.sample b/.kamal/hooks/pre-proxy-reboot.sample new file mode 100755 index 0000000..061f805 --- /dev/null +++ b/.kamal/hooks/pre-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooting kamal-proxy on $KAMAL_HOSTS..." diff --git a/.kamal/secrets b/.kamal/secrets new file mode 100644 index 0000000..9a771a3 --- /dev/null +++ b/.kamal/secrets @@ -0,0 +1,17 @@ +# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets, +# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either +# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. + +# Example of extracting secrets from 1password (or another compatible pw manager) +# SECRETS=$(kamal secrets fetch --adapter 1password --account your-account --from Vault/Item KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY) +# KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${SECRETS}) +# RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY ${SECRETS}) + +# Use a GITHUB_TOKEN if private repositories are needed for the image +# GITHUB_TOKEN=$(gh config get -h github.com oauth_token) + +# Grab the registry password from ENV +KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD + +# Improve security by using a password manager. Never check config/master.key into git! +RAILS_MASTER_KEY=$(cat config/master.key) diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..0373daa --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +23.6.0 diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..f9d86d4 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,8 @@ +# Omakase Ruby styling for Rails +inherit_gem: { rubocop-rails-omakase: rubocop.yml } + +# Overwrite or add rules to create your own house style +# +# # Use `[a, [b, c]]` not `[ a, [ b, c ] ]` +# Layout/SpaceInsideArrayLiteralBrackets: +# Enabled: false diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..f989260 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.4.4 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a137131 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,82 @@ +# syntax=docker/dockerfile:1 +# check=error=true + +# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand: +# docker build -t theseus . +# docker run -d -p 80:80 -e RAILS_MASTER_KEY= --name theseus theseus + +# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html + +# Make sure RUBY_VERSION matches the Ruby version in .ruby-version +ARG RUBY_VERSION=3.4.4 +FROM docker.io/library/ruby:$RUBY_VERSION-slim + +# Set production environment +ENV RAILS_ENV="production" \ + BUNDLE_DEPLOYMENT="1" \ + BUNDLE_PATH="/usr/local/bundle" \ + BUNDLE_WITHOUT="development" + +# Install base packages and build dependencies +RUN apt-get update -qq && apt-get install --no-install-recommends -y \ + curl \ + libjemalloc2 \ + libvips \ + postgresql-client \ + wget \ + build-essential \ + git \ + libpq-dev \ + node-gyp \ + pkg-config \ + python-is-python3 \ + imagemagick \ + libmagickwand-dev \ + ghostscript \ + libyaml-dev \ + chromium + +RUN sed -i '/disable ghostscript format types/,+6d' /etc/ImageMagick-6/policy.xml + +# Install Node.js and Yarn +ARG NODE_VERSION=23.6.0 +ARG YARN_VERSION=1.22.22 +ENV PATH=/usr/local/node/bin:$PATH +RUN curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz -C /tmp/ && \ + /tmp/node-build-master/bin/node-build "${NODE_VERSION}" /usr/local/node && \ + npm install -g yarn@$YARN_VERSION && \ + rm -rf /tmp/node-build-master + +# Rails app lives here +WORKDIR /rails + +# Install application gems +COPY Gemfile Gemfile.lock ./ +RUN bundle install && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ + bundle exec bootsnap precompile --gemfile + +# Install node modules +COPY package.json yarn.lock ./ +RUN yarn install --frozen-lockfile + +# Copy application code +COPY . . + +# Precompile bootsnap code and assets +RUN bundle exec bootsnap precompile app/ lib/ && \ + SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile && \ + rm -rf node_modules + +# Run and own only the runtime files as a non-root user for security +RUN groupadd --system --gid 1000 rails && \ + useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ + chown -R rails:rails db log storage tmp +USER 1000:1000 + +# Entrypoint prepares the database. +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +# Start server via Thruster by default, this can be overwritten at runtime +EXPOSE 80 +CMD ["./bin/thrust", "./bin/rails", "server"] diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..4fa8924 --- /dev/null +++ b/Gemfile @@ -0,0 +1,171 @@ +source "https://rubygems.org" + +# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" +gem "rails", "~> 8.0.1" +# The modern asset pipeline for Rails [https://github.com/rails/propshaft] +gem "propshaft" +# Use postgresql as the database for Active Record +gem "pg", "~> 1.1" +# Use the Puma web server [https://github.com/puma/puma] +gem "puma", ">= 5.0" +# Bundle and transpile JavaScript [https://github.com/rails/jsbundling-rails] +# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] +gem "turbo-rails" +# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] +gem "stimulus-rails" +# Build JSON APIs with ease [https://github.com/rails/jbuilder] +gem "jbuilder", "~> 2.13" + +# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] +# gem "bcrypt", "~> 3.1.7" + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem "tzinfo-data", platforms: %i[ windows jruby ] + +# Use the database-backed adapters for Rails.cache, Active Job, and Action Cable +gem "solid_cache" +gem "solid_queue" +gem "solid_cable" + +# Reduces boot times through caching; required in config/boot.rb +gem "bootsnap", require: false + +# Deploy this application anywhere as a Docker container [https://kamal-deploy.org] +gem "kamal", require: false + +# Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/] +gem "thruster", require: false + +# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] +# gem "image_processing", "~> 1.2" + +group :development, :test do + # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem + gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" + + # Static analysis for security vulnerabilities [https://brakemanscanner.org/] + gem "brakeman", require: false + + # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] + gem "rubocop-rails-omakase", require: false +end + +group :development do + # Use console on exceptions pages [https://github.com/rails/web-console] + gem "web-console" +end + +group :test do + # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] + gem "capybara" + gem "selenium-webdriver" +end + +gem "foreman", "~> 0.88.1" + +gem "dotenv-rails", "~> 3.1" + +gem "net-http", "~> 0.6.0" + +gem "http", "~> 5.2" + +gem "annotaterb", "~> 4.14" + +gem "aasm", "~> 5.5" + +gem "norairrecord", "~> 0.3.0" + +gem "filterrific", "~> 5.2" + +gem "hashid-rails", "~> 1.4" + +gem "csv", "~> 3.3" + +gem "faraday", "~> 2.12" + +gem "oauth2", "~> 2.0" + +gem "snail", "~> 2.3" + +gem "easypost", "~> 6.4" + +gem "pundit", "~> 2.5" + + + + +gem "select2-rails", "~> 4.0" + +gem "jquery-rails", "~> 4.6" + + +gem "country-select", "~> 1.2" + +gem "countries", "~> 7.1" + +gem "good_job", "~> 4.9" + +gem "awesome_print", "~> 1.9" + +gem "cocoon", "~> 1.2" + +gem "administrate", "~> 0.19.0" + +gem "slim-rails", "~> 3.7" + +group :development do + gem "letter_opener_web", "~> 3.0" +end + +gem "prawn", "~> 2.5" + +gem "usps_intelligent_barcode", "~> 1.0" + +gem "rqrcode", "~> 2.2" + +gem "kaminari", "~> 1.2" + +gem "aws-sdk-s3", require: false + +gem "acts-as-taggable-array-on", "~> 0.7.0" + +gem "selectize-rails", "~> 0.12.6" + +gem "ivymeter", "~> 0.1.0" + +gem "slack-notifier", "~> 2.4" + +gem "nokogiri", "~> 1.18" + +gem "vite_rails" + +gem "blazer", "~> 3.3" + + +gem "redis", "~> 5.4" + +gem "valid_email2", "~> 7.0" + +gem "sssecrets", "~> 1.0" + +gem "lockbox", "~> 2.0" + +gem "blind_index", "~> 2.7" + +gem "ruby-openai", "~> 8.1" + +gem "parallel", "~> 1.26" + +gem "honeybadger", "~> 5.28" + +gem "rmagick", "~> 5.3" + +gem "jb", "~> 0.8.2" + +gem "ferrum_pdf", "~> 0.3.0" + +gem "literal", "~> 1.7" + +gem "phlex-rails", "~> 2.2" + +gem "xsv", "~> 1.3" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..d9532f2 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,702 @@ +GEM + remote: https://rubygems.org/ + specs: + aasm (5.5.0) + concurrent-ruby (~> 1.0) + actioncable (8.0.2) + actionpack (= 8.0.2) + activesupport (= 8.0.2) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (8.0.2) + actionpack (= 8.0.2) + activejob (= 8.0.2) + activerecord (= 8.0.2) + activestorage (= 8.0.2) + activesupport (= 8.0.2) + mail (>= 2.8.0) + actionmailer (8.0.2) + actionpack (= 8.0.2) + actionview (= 8.0.2) + activejob (= 8.0.2) + activesupport (= 8.0.2) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.0.2) + actionview (= 8.0.2) + activesupport (= 8.0.2) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (8.0.2) + actionpack (= 8.0.2) + activerecord (= 8.0.2) + activestorage (= 8.0.2) + activesupport (= 8.0.2) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.0.2) + activesupport (= 8.0.2) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.0.2) + activesupport (= 8.0.2) + globalid (>= 0.3.6) + activemodel (8.0.2) + activesupport (= 8.0.2) + activerecord (8.0.2) + activemodel (= 8.0.2) + activesupport (= 8.0.2) + timeout (>= 0.4.0) + activestorage (8.0.2) + actionpack (= 8.0.2) + activejob (= 8.0.2) + activerecord (= 8.0.2) + activesupport (= 8.0.2) + marcel (~> 1.0) + activesupport (8.0.2) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + acts-as-taggable-array-on (0.7.0) + activerecord (>= 5.2) + activesupport (>= 5.2) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + administrate (0.19.0) + actionpack (>= 5.0) + actionview (>= 5.0) + activerecord (>= 5.0) + jquery-rails (>= 4.0) + kaminari (>= 1.0) + sassc-rails (~> 2.1) + selectize-rails (~> 0.6) + andand (1.3.3) + annotaterb (4.14.0) + argon2-kdf (0.3.1) + fiddle + ast (2.4.3) + awesome_print (1.9.2) + aws-eventstream (1.3.2) + aws-partitions (1.1106.0) + aws-sdk-core (3.224.0) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + jmespath (~> 1, >= 1.6.1) + logger + aws-sdk-kms (1.101.0) + aws-sdk-core (~> 3, >= 3.216.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.186.1) + aws-sdk-core (~> 3, >= 3.216.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.11.0) + aws-eventstream (~> 1, >= 1.0.2) + base64 (0.2.0) + bcrypt_pbkdf (1.1.1) + bcrypt_pbkdf (1.1.1-arm64-darwin) + bcrypt_pbkdf (1.1.1-x86_64-darwin) + benchmark (0.4.0) + bigdecimal (3.1.9) + bindex (0.8.1) + blazer (3.3.0) + activerecord (>= 7.1) + chartkick (>= 5) + csv + railties (>= 7.1) + safely_block (>= 0.4) + blind_index (2.7.0) + activesupport (>= 7.1) + argon2-kdf (>= 0.2) + bootsnap (1.18.6) + msgpack (~> 1.2) + brakeman (7.0.2) + racc + builder (3.3.0) + capybara (3.40.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.11) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + chartkick (5.1.5) + childprocess (5.1.0) + logger (~> 1.5) + chunky_png (1.4.0) + cocoon (1.2.15) + concurrent-ruby (1.3.5) + connection_pool (2.5.3) + countries (7.1.1) + unaccent (~> 0.3) + country-select (1.2.1) + crass (1.0.6) + csv (3.3.4) + date (3.4.1) + debug (1.10.0) + irb (~> 1.10) + reline (>= 0.3.8) + domain_name (0.6.20240107) + dotenv (3.1.8) + dotenv-rails (3.1.8) + dotenv (= 3.1.8) + railties (>= 6.1) + drb (2.2.1) + dry-cli (1.2.0) + easypost (6.4.1) + ed25519 (1.4.0) + erb (5.0.1) + erubi (1.13.1) + et-orbi (1.2.11) + tzinfo + event_stream_parser (1.0.0) + faraday (2.13.1) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-multipart (1.1.0) + multipart-post (~> 2.0) + faraday-net_http (3.4.0) + net-http (>= 0.5.0) + faraday-net_http_persistent (2.3.0) + faraday (~> 2.5) + net-http-persistent (>= 4.0.4, < 5) + ferrum (0.17.1) + addressable (~> 2.5) + base64 (~> 0.2) + concurrent-ruby (~> 1.1) + webrick (~> 1.7) + websocket-driver (~> 0.7) + ferrum_pdf (0.3.0) + ferrum (~> 0.15) + rails (>= 6.0.0) + ffi (1.17.2-aarch64-linux-gnu) + ffi (1.17.2-aarch64-linux-musl) + ffi (1.17.2-arm-linux-gnu) + ffi (1.17.2-arm-linux-musl) + ffi (1.17.2-arm64-darwin) + ffi (1.17.2-x86_64-darwin) + ffi (1.17.2-x86_64-linux-gnu) + ffi (1.17.2-x86_64-linux-musl) + ffi-compiler (1.3.2) + ffi (>= 1.15.5) + rake + fiddle (1.1.8) + filterrific (5.2.7) + foreman (0.88.1) + fugit (1.11.1) + et-orbi (~> 1, >= 1.2.11) + raabro (~> 1.4) + globalid (1.2.1) + activesupport (>= 6.1) + good_job (4.10.1) + activejob (>= 6.1.0) + activerecord (>= 6.1.0) + concurrent-ruby (>= 1.3.1) + fugit (>= 1.11.0) + railties (>= 6.1.0) + thor (>= 1.0.0) + hashid-rails (1.4.1) + activerecord (>= 4.0) + hashids (~> 1.0) + hashids (1.0.6) + hashie (5.0.0) + honeybadger (5.28.0) + logger + ostruct + http (5.2.0) + addressable (~> 2.8) + base64 (~> 0.1) + http-cookie (~> 1.0) + http-form_data (~> 2.2) + llhttp-ffi (~> 0.5.0) + http-cookie (1.0.8) + domain_name (~> 0.5) + http-form_data (2.3.0) + i18n (1.14.7) + concurrent-ruby (~> 1.0) + io-console (0.8.0) + irb (1.15.2) + pp (>= 0.6.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + ivymeter (0.1.0) + jb (0.8.2) + jbuilder (2.13.0) + actionview (>= 5.0.0) + activesupport (>= 5.0.0) + jmespath (1.6.2) + jquery-rails (4.6.0) + rails-dom-testing (>= 1, < 3) + railties (>= 4.2.0) + thor (>= 0.14, < 2.0) + json (2.12.0) + jwt (2.10.1) + base64 + kamal (2.6.1) + activesupport (>= 7.0) + base64 (~> 0.2) + bcrypt_pbkdf (~> 1.0) + concurrent-ruby (~> 1.2) + dotenv (~> 3.1) + ed25519 (~> 1.4) + net-ssh (~> 7.3) + sshkit (>= 1.23.0, < 2.0) + thor (~> 1.3) + zeitwerk (>= 2.6.18, < 3.0) + kaminari (1.2.2) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.2.2) + kaminari-activerecord (= 1.2.2) + kaminari-core (= 1.2.2) + kaminari-actionview (1.2.2) + actionview + kaminari-core (= 1.2.2) + kaminari-activerecord (1.2.2) + activerecord + kaminari-core (= 1.2.2) + kaminari-core (1.2.2) + language_server-protocol (3.17.0.5) + launchy (3.1.1) + addressable (~> 2.8) + childprocess (~> 5.0) + logger (~> 1.6) + letter_opener (1.10.0) + launchy (>= 2.2, < 4) + letter_opener_web (3.0.0) + actionmailer (>= 6.1) + letter_opener (~> 1.9) + railties (>= 6.1) + rexml + lint_roller (1.1.0) + literal (1.7.1) + zeitwerk + llhttp-ffi (0.5.1) + ffi-compiler (~> 1.0) + rake (~> 13.0) + lockbox (2.0.1) + logger (1.7.0) + loofah (2.24.1) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.8.1) + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.0.4) + matrix (0.4.2) + memoizer (1.0.3) + mini_mime (1.1.5) + minitest (5.25.5) + msgpack (1.8.0) + multi_xml (0.7.2) + bigdecimal (~> 3.1) + multipart-post (2.4.1) + mutex_m (0.3.0) + net-http (0.6.0) + uri + net-http-persistent (4.0.5) + connection_pool (~> 2.2) + net-imap (0.5.8) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-scp (4.1.0) + net-ssh (>= 2.6.5, < 8.0.0) + net-sftp (4.0.0) + net-ssh (>= 5.0.0, < 8.0.0) + net-smtp (0.5.1) + net-protocol + net-ssh (7.3.0) + nio4r (2.7.4) + nokogiri (1.18.8-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.8-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.18.8-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.8-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.18.8-arm64-darwin) + racc (~> 1.4) + nokogiri (1.18.8-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.18.8-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.8-x86_64-linux-musl) + racc (~> 1.4) + norairrecord (0.3.0) + faraday (>= 1.0, < 3.0) + faraday-net_http_persistent + net-http-persistent + oauth2 (2.0.10) + faraday (>= 0.17.3, < 4.0) + jwt (>= 1.0, < 4.0) + logger (~> 1.2) + multi_xml (~> 0.5) + rack (>= 1.2, < 4) + snaky_hash (~> 2.0) + version_gem (>= 1.1.8, < 3) + observer (0.1.2) + ostruct (0.6.1) + parallel (1.27.0) + parser (3.3.8.0) + ast (~> 2.4.1) + racc + pdf-core (0.10.0) + pg (1.5.9) + phlex (2.2.1) + zeitwerk (~> 2.7) + phlex-rails (2.2.0) + phlex (~> 2.2.1) + railties (>= 7.1, < 9) + pkg-config (1.6.2) + pp (0.6.2) + prettyprint + prawn (2.5.0) + matrix (~> 0.4) + pdf-core (~> 0.10.0) + ttfunk (~> 1.8) + prettyprint (0.2.0) + prism (1.4.0) + propshaft (1.1.0) + actionpack (>= 7.0.0) + activesupport (>= 7.0.0) + rack + railties (>= 7.0.0) + psych (5.2.6) + date + stringio + public_suffix (6.0.2) + puma (6.6.0) + nio4r (~> 2.0) + pundit (2.5.0) + activesupport (>= 3.0.0) + raabro (1.4.0) + racc (1.8.1) + rack (3.1.15) + rack-proxy (0.7.7) + rack + rack-session (2.1.1) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.2.1) + rack (>= 3) + rails (8.0.2) + actioncable (= 8.0.2) + actionmailbox (= 8.0.2) + actionmailer (= 8.0.2) + actionpack (= 8.0.2) + actiontext (= 8.0.2) + actionview (= 8.0.2) + activejob (= 8.0.2) + activemodel (= 8.0.2) + activerecord (= 8.0.2) + activestorage (= 8.0.2) + activesupport (= 8.0.2) + bundler (>= 1.15.0) + railties (= 8.0.2) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.2) + loofah (~> 2.21) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (8.0.2) + actionpack (= 8.0.2) + activesupport (= 8.0.2) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.2.1) + rdoc (6.14.0) + erb + psych (>= 4.0.0) + redis (5.4.0) + redis-client (>= 0.22.0) + redis-client (0.24.0) + connection_pool + regexp_parser (2.10.0) + reline (0.6.1) + io-console (~> 0.5) + rexml (3.4.1) + rmagick (5.5.0) + observer (~> 0.1) + pkg-config (~> 1.4) + rqrcode (2.2.0) + chunky_png (~> 1.0) + rqrcode_core (~> 1.0) + rqrcode_core (1.2.0) + rubocop (1.75.6) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.44.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.44.1) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-performance (1.25.0) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) + rubocop-rails (2.32.0) + activesupport (>= 4.2.0) + lint_roller (~> 1.1) + rack (>= 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) + rubocop-rails-omakase (1.1.0) + rubocop (>= 1.72) + rubocop-performance (>= 1.24) + rubocop-rails (>= 2.30) + ruby-openai (8.1.0) + event_stream_parser (>= 0.3.0, < 2.0.0) + faraday (>= 1) + faraday-multipart (>= 1) + ruby-progressbar (1.13.0) + rubyzip (2.4.1) + safely_block (0.4.1) + sassc (2.4.0) + ffi (~> 1.9) + sassc-rails (2.1.2) + railties (>= 4.0.0) + sassc (>= 2.0) + sprockets (> 3.0) + sprockets-rails + tilt + securerandom (0.4.1) + select2-rails (4.0.13) + selectize-rails (0.12.6) + selenium-webdriver (4.32.0) + base64 (~> 0.2) + logger (~> 1.4) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) + slack-notifier (2.4.0) + slim (5.2.1) + temple (~> 0.10.0) + tilt (>= 2.1.0) + slim-rails (3.7.0) + actionpack (>= 3.1) + railties (>= 3.1) + slim (>= 3.0, < 6.0, != 5.0.0) + snail (2.3.0) + activesupport + snaky_hash (2.0.1) + hashie + version_gem (~> 1.1, >= 1.1.1) + solid_cable (3.0.8) + actioncable (>= 7.2) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_cache (1.0.7) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_queue (1.1.5) + activejob (>= 7.1) + activerecord (>= 7.1) + concurrent-ruby (>= 1.3.1) + fugit (~> 1.11.0) + railties (>= 7.1) + thor (~> 1.3.1) + sprockets (4.2.2) + concurrent-ruby (~> 1.0) + logger + rack (>= 2.2.4, < 4) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) + sprockets (>= 3.0.0) + sshkit (1.24.0) + base64 + logger + net-scp (>= 1.1.2) + net-sftp (>= 2.1.2) + net-ssh (>= 2.8.0) + ostruct + sssecrets (1.0.1) + stimulus-rails (1.3.4) + railties (>= 6.0.0) + stringio (3.1.7) + temple (0.10.3) + thor (1.3.2) + thruster (0.1.13) + thruster (0.1.13-aarch64-linux) + thruster (0.1.13-arm64-darwin) + thruster (0.1.13-x86_64-darwin) + thruster (0.1.13-x86_64-linux) + tilt (2.6.0) + timeout (0.4.3) + ttfunk (1.8.0) + bigdecimal (~> 3.1) + turbo-rails (2.0.13) + actionpack (>= 7.1.0) + railties (>= 7.1.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unaccent (0.4.0) + unicode-display_width (3.1.4) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) + uri (1.0.3) + useragent (0.16.11) + usps_intelligent_barcode (1.0.0) + andand (~> 1.3) + memoizer (~> 1.0) + valid_email2 (7.0.13) + activemodel (>= 6.0) + mail (~> 2.5) + version_gem (1.1.8) + vite_rails (3.0.19) + railties (>= 5.1, < 9) + vite_ruby (~> 3.0, >= 3.2.2) + vite_ruby (3.9.2) + dry-cli (>= 0.7, < 2) + logger (~> 1.6) + mutex_m + rack-proxy (~> 0.6, >= 0.6.1) + zeitwerk (~> 2.2) + web-console (4.2.1) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) + bindex (>= 0.4.0) + railties (>= 6.0.0) + webrick (1.9.1) + websocket (1.2.11) + websocket-driver (0.7.7) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + xsv (1.3.2) + rubyzip (>= 1.3, < 3) + zeitwerk (2.7.3) + +PLATFORMS + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin + x86_64-darwin + x86_64-linux + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + aasm (~> 5.5) + acts-as-taggable-array-on (~> 0.7.0) + administrate (~> 0.19.0) + annotaterb (~> 4.14) + awesome_print (~> 1.9) + aws-sdk-s3 + blazer (~> 3.3) + blind_index (~> 2.7) + bootsnap + brakeman + capybara + cocoon (~> 1.2) + countries (~> 7.1) + country-select (~> 1.2) + csv (~> 3.3) + debug + dotenv-rails (~> 3.1) + easypost (~> 6.4) + faraday (~> 2.12) + ferrum_pdf (~> 0.3.0) + filterrific (~> 5.2) + foreman (~> 0.88.1) + good_job (~> 4.9) + hashid-rails (~> 1.4) + honeybadger (~> 5.28) + http (~> 5.2) + ivymeter (~> 0.1.0) + jb (~> 0.8.2) + jbuilder (~> 2.13) + jquery-rails (~> 4.6) + kamal + kaminari (~> 1.2) + letter_opener_web (~> 3.0) + literal (~> 1.7) + lockbox (~> 2.0) + net-http (~> 0.6.0) + nokogiri (~> 1.18) + norairrecord (~> 0.3.0) + oauth2 (~> 2.0) + parallel (~> 1.26) + pg (~> 1.1) + phlex-rails (~> 2.2) + prawn (~> 2.5) + propshaft + puma (>= 5.0) + pundit (~> 2.5) + rails (~> 8.0.1) + redis (~> 5.4) + rmagick (~> 5.3) + rqrcode (~> 2.2) + rubocop-rails-omakase + ruby-openai (~> 8.1) + select2-rails (~> 4.0) + selectize-rails (~> 0.12.6) + selenium-webdriver + slack-notifier (~> 2.4) + slim-rails (~> 3.7) + snail (~> 2.3) + solid_cable + solid_cache + solid_queue + sssecrets (~> 1.0) + stimulus-rails + thruster + turbo-rails + tzinfo-data + usps_intelligent_barcode (~> 1.0) + valid_email2 (~> 7.0) + vite_rails + web-console + xsv (~> 1.3) + +BUNDLED WITH + 2.6.9 diff --git a/Procfile.dev b/Procfile.dev new file mode 100644 index 0000000..49d0d1b --- /dev/null +++ b/Procfile.dev @@ -0,0 +1,2 @@ +web: env RUBY_DEBUG_OPEN=true bin/rails server +vite: bin/vite dev \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a037c35 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# hack club's shiny new mail system! +it's open source now i guess + +getting this running if you're not me or production is probably kind of a bear! + +i will write seeds and a way to log in without setting up a slack....at...some point..... + +(possibly not soon due to time constraints) + +if you wanna hack on this and contribute, dm me (@nora) on slack, i'd love the help! + +a lot of the design decisions and code are not great. they will be incrementally improved someday. (you can look towards issues for some of that) + +thanks for being here! + +~nora <3 \ No newline at end of file diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..9a5ea73 --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative "config/application" + +Rails.application.load_tasks diff --git a/app/assets/builds/.keep b/app/assets/builds/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js new file mode 100644 index 0000000..e69de29 diff --git a/app/components/base.rb b/app/components/base.rb new file mode 100644 index 0000000..dfe3e7b --- /dev/null +++ b/app/components/base.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class Components::Base < Phlex::HTML + include Components + + # Include any helpers you want to be available across all components + include Phlex::Rails::Helpers::Routes + + if Rails.env.development? + def before_template + comment { "Before #{self.class.name}" } + super + end + end +end diff --git a/app/controllers/admin/addresses_controller.rb b/app/controllers/admin/addresses_controller.rb new file mode 100644 index 0000000..e71d5a2 --- /dev/null +++ b/app/controllers/admin/addresses_controller.rb @@ -0,0 +1,46 @@ +module Admin + class AddressesController < Admin::ApplicationController + # Overwrite any of the RESTful controller actions to implement custom behavior + # For example, you may want to send an email after a foo is updated. + # + # def update + # super + # send_foo_updated_email(requested_resource) + # end + + # Override this method to specify custom lookup behavior. + # This will be used to set the resource for the `show`, `edit`, and `update` + # actions. + # + # def find_resource(param) + # Foo.find_by!(slug: param) + # end + + # The result of this lookup will be available as `requested_resource` + + # Override this if you have certain roles that require a subset + # this will be used to set the records shown on the `index` action. + # + # def scoped_resource + # if current_user.super_admin? + # resource_class + # else + # resource_class.with_less_stuff + # end + # end + + # Override `resource_params` if you want to transform the submitted + # data before it's persisted. For example, the following would turn all + # empty values into nil values. It uses other APIs such as `resource_class` + # and `dashboard`: + # + # def resource_params + # params.require(resource_class.model_name.param_key). + # permit(dashboard.permitted_attributes(action_name)). + # transform_values { |value| value == "" ? nil : value } + # end + + # See https://administrate-demo.herokuapp.com/customizing_controller_actions + # for more information + end +end diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb new file mode 100644 index 0000000..8818cf4 --- /dev/null +++ b/app/controllers/admin/application_controller.rb @@ -0,0 +1,21 @@ +# All Administrate controllers inherit from this +# `Administrate::ApplicationController`, making it the ideal place to put +# authentication logic or other before_actions. +# +# If you want to add pagination or other controller-level concerns, +# you're free to overwrite the RESTful controller actions. +module Admin + class ApplicationController < Administrate::ApplicationController + before_action :authenticate_admin + + def authenticate_admin + redirect_to root_path, alert: "you can't do that!" unless current_user&.admin? + end + + helper_method :current_user + + def current_user + @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id] + end + end +end diff --git a/app/controllers/admin/common_tags_controller.rb b/app/controllers/admin/common_tags_controller.rb new file mode 100644 index 0000000..f44f6c2 --- /dev/null +++ b/app/controllers/admin/common_tags_controller.rb @@ -0,0 +1,46 @@ +module Admin + class CommonTagsController < Admin::ApplicationController + # Overwrite any of the RESTful controller actions to implement custom behavior + # For example, you may want to send an email after a foo is updated. + # + # def update + # super + # send_foo_updated_email(requested_resource) + # end + + # Override this method to specify custom lookup behavior. + # This will be used to set the resource for the `show`, `edit`, and `update` + # actions. + # + # def find_resource(param) + # Foo.find_by!(slug: param) + # end + + # The result of this lookup will be available as `requested_resource` + + # Override this if you have certain roles that require a subset + # this will be used to set the records shown on the `index` action. + # + # def scoped_resource + # if current_user.super_admin? + # resource_class + # else + # resource_class.with_less_stuff + # end + # end + + # Override `resource_params` if you want to transform the submitted + # data before it's persisted. For example, the following would turn all + # empty values into nil values. It uses other APIs such as `resource_class` + # and `dashboard`: + # + # def resource_params + # params.require(resource_class.model_name.param_key). + # permit(dashboard.permitted_attributes(action_name)). + # transform_values { |value| value == "" ? nil : value } + # end + + # See https://administrate-demo.herokuapp.com/customizing_controller_actions + # for more information + end +end diff --git a/app/controllers/admin/return_addresses_controller.rb b/app/controllers/admin/return_addresses_controller.rb new file mode 100644 index 0000000..67886d6 --- /dev/null +++ b/app/controllers/admin/return_addresses_controller.rb @@ -0,0 +1,46 @@ +module Admin + class ReturnAddressesController < Admin::ApplicationController + # Overwrite any of the RESTful controller actions to implement custom behavior + # For example, you may want to send an email after a foo is updated. + # + # def update + # super + # send_foo_updated_email(requested_resource) + # end + + # Override this method to specify custom lookup behavior. + # This will be used to set the resource for the `show`, `edit`, and `update` + # actions. + # + # def find_resource(param) + # Foo.find_by!(slug: param) + # end + + # The result of this lookup will be available as `requested_resource` + + # Override this if you have certain roles that require a subset + # this will be used to set the records shown on the `index` action. + # + # def scoped_resource + # if current_user.super_admin? + # resource_class + # else + # resource_class.with_less_stuff + # end + # end + + # Override `resource_params` if you want to transform the submitted + # data before it's persisted. For example, the following would turn all + # empty values into nil values. It uses other APIs such as `resource_class` + # and `dashboard`: + # + # def resource_params + # params.require(resource_class.model_name.param_key). + # permit(dashboard.permitted_attributes(action_name)). + # transform_values { |value| value == "" ? nil : value } + # end + + # See https://administrate-demo.herokuapp.com/customizing_controller_actions + # for more information + end +end diff --git a/app/controllers/admin/source_tags_controller.rb b/app/controllers/admin/source_tags_controller.rb new file mode 100644 index 0000000..f40cbb8 --- /dev/null +++ b/app/controllers/admin/source_tags_controller.rb @@ -0,0 +1,46 @@ +module Admin + class SourceTagsController < Admin::ApplicationController + # Overwrite any of the RESTful controller actions to implement custom behavior + # For example, you may want to send an email after a foo is updated. + # + # def update + # super + # send_foo_updated_email(requested_resource) + # end + + # Override this method to specify custom lookup behavior. + # This will be used to set the resource for the `show`, `edit`, and `update` + # actions. + # + # def find_resource(param) + # Foo.find_by!(slug: param) + # end + + # The result of this lookup will be available as `requested_resource` + + # Override this if you have certain roles that require a subset + # this will be used to set the records shown on the `index` action. + # + # def scoped_resource + # if current_user.super_admin? + # resource_class + # else + # resource_class.with_less_stuff + # end + # end + + # Override `resource_params` if you want to transform the submitted + # data before it's persisted. For example, the following would turn all + # empty values into nil values. It uses other APIs such as `resource_class` + # and `dashboard`: + # + # def resource_params + # params.require(resource_class.model_name.param_key). + # permit(dashboard.permitted_attributes(action_name)). + # transform_values { |value| value == "" ? nil : value } + # end + + # See https://administrate-demo.herokuapp.com/customizing_controller_actions + # for more information + end +end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb new file mode 100644 index 0000000..2c4ab7d --- /dev/null +++ b/app/controllers/admin/users_controller.rb @@ -0,0 +1,46 @@ +module Admin + class UsersController < Admin::ApplicationController + # Overwrite any of the RESTful controller actions to implement custom behavior + # For example, you may want to send an email after a foo is updated. + # + # def update + # super + # send_foo_updated_email(requested_resource) + # end + + # Override this method to specify custom lookup behavior. + # This will be used to set the resource for the `show`, `edit`, and `update` + # actions. + # + # def find_resource(param) + # Foo.find_by!(slug: param) + # end + + # The result of this lookup will be available as `requested_resource` + + # Override this if you have certain roles that require a subset + # this will be used to set the records shown on the `index` action. + # + # def scoped_resource + # if current_user.super_admin? + # resource_class + # else + # resource_class.with_less_stuff + # end + # end + + # Override `resource_params` if you want to transform the submitted + # data before it's persisted. For example, the following would turn all + # empty values into nil values. It uses other APIs such as `resource_class` + # and `dashboard`: + # + # def resource_params + # params.require(resource_class.model_name.param_key). + # permit(dashboard.permitted_attributes(action_name)). + # transform_values { |value| value == "" ? nil : value } + # end + + # See https://administrate-demo.herokuapp.com/customizing_controller_actions + # for more information + end +end diff --git a/app/controllers/admin/usps/mailer_ids_controller.rb b/app/controllers/admin/usps/mailer_ids_controller.rb new file mode 100644 index 0000000..6c992e0 --- /dev/null +++ b/app/controllers/admin/usps/mailer_ids_controller.rb @@ -0,0 +1,46 @@ +module Admin + class USPS::MailerIdsController < Admin::ApplicationController + # Overwrite any of the RESTful controller actions to implement custom behavior + # For example, you may want to send an email after a foo is updated. + # + # def update + # super + # send_foo_updated_email(requested_resource) + # end + + # Override this method to specify custom lookup behavior. + # This will be used to set the resource for the `show`, `edit`, and `update` + # actions. + # + # def find_resource(param) + # Foo.find_by!(slug: param) + # end + + # The result of this lookup will be available as `requested_resource` + + # Override this if you have certain roles that require a subset + # this will be used to set the records shown on the `index` action. + # + # def scoped_resource + # if current_user.super_admin? + # resource_class + # else + # resource_class.with_less_stuff + # end + # end + + # Override `resource_params` if you want to transform the submitted + # data before it's persisted. For example, the following would turn all + # empty values into nil values. It uses other APIs such as `resource_class` + # and `dashboard`: + # + # def resource_params + # params.require(resource_class.model_name.param_key). + # permit(dashboard.permitted_attributes(action_name)). + # transform_values { |value| value == "" ? nil : value } + # end + + # See https://administrate-demo.herokuapp.com/customizing_controller_actions + # for more information + end +end diff --git a/app/controllers/admin/usps/payment_accounts_controller.rb b/app/controllers/admin/usps/payment_accounts_controller.rb new file mode 100644 index 0000000..106d314 --- /dev/null +++ b/app/controllers/admin/usps/payment_accounts_controller.rb @@ -0,0 +1,48 @@ +module Admin + module USPS + class PaymentAccountsController < Admin::ApplicationController + # Overwrite any of the RESTful controller actions to implement custom behavior + # For example, you may want to send an email after a foo is updated. + # + # def update + # super + # send_foo_updated_email(requested_resource) + # end + + # Override this method to specify custom lookup behavior. + # This will be used to set the resource for the `show`, `edit`, and `update` + # actions. + # + # def find_resource(param) + # Foo.find_by!(slug: param) + # end + + # The result of this lookup will be available as `requested_resource` + + # Override this if you have certain roles that require a subset + # this will be used to set the records shown on the `index` action. + # + # def scoped_resource + # if current_user.super_admin? + # resource_class + # else + # resource_class.with_less_stuff + # end + # end + + # Override `resource_params` if you want to transform the submitted + # data before it's persisted. For example, the following would turn all + # empty values into nil values. It uses other APIs such as `resource_class` + # and `dashboard`: + # + # def resource_params + # params.require(resource_class.model_name.param_key). + # permit(dashboard.permitted_attributes(action_name)). + # transform_values { |value| value == "" ? nil : value } + # end + + # See https://administrate-demo.herokuapp.com/customizing_controller_actions + # for more information + end + end +end diff --git a/app/controllers/admin/warehouse/line_items_controller.rb b/app/controllers/admin/warehouse/line_items_controller.rb new file mode 100644 index 0000000..7daf126 --- /dev/null +++ b/app/controllers/admin/warehouse/line_items_controller.rb @@ -0,0 +1,48 @@ +module Admin + module Warehouse + class LineItemsController < Admin::ApplicationController + # Overwrite any of the RESTful controller actions to implement custom behavior + # For example, you may want to send an email after a foo is updated. + # + # def update + # super + # send_foo_updated_email(requested_resource) + # end + + # Override this method to specify custom lookup behavior. + # This will be used to set the resource for the `show`, `edit`, and `update` + # actions. + # + # def find_resource(param) + # Foo.find_by!(slug: param) + # end + + # The result of this lookup will be available as `requested_resource` + + # Override this if you have certain roles that require a subset + # this will be used to set the records shown on the `index` action. + # + # def scoped_resource + # if current_user.super_admin? + # resource_class + # else + # resource_class.with_less_stuff + # end + # end + + # Override `resource_params` if you want to transform the submitted + # data before it's persisted. For example, the following would turn all + # empty values into nil values. It uses other APIs such as `resource_class` + # and `dashboard`: + # + # def resource_params + # params.require(resource_class.model_name.param_key). + # permit(dashboard.permitted_attributes(action_name)). + # transform_values { |value| value == "" ? nil : value } + # end + + # See https://administrate-demo.herokuapp.com/customizing_controller_actions + # for more information + end + end +end diff --git a/app/controllers/admin/warehouse/orders_controller.rb b/app/controllers/admin/warehouse/orders_controller.rb new file mode 100644 index 0000000..5ec6f54 --- /dev/null +++ b/app/controllers/admin/warehouse/orders_controller.rb @@ -0,0 +1,49 @@ +module Admin + module Warehouse + class OrdersController < Admin::ApplicationController + include Administrate::ArrayParameterHandler + # Overwrite any of the RESTful controller actions to implement custom behavior + # For example, you may want to send an email after a foo is updated. + # + # def update + # super + # send_foo_updated_email(requested_resource) + # end + + # Override this method to specify custom lookup behavior. + # This will be used to set the resource for the `show`, `edit`, and `update` + # actions. + # + def find_resource(param) + ::Warehouse::Order.find_by!(hc_id: param) + end + + # The result of this lookup will be available as `requested_resource` + + # Override this if you have certain roles that require a subset + # this will be used to set the records shown on the `index` action. + # + # def scoped_resource + # if current_user.super_admin? + # resource_class + # else + # resource_class.with_less_stuff + # end + # end + + # Override `resource_params` if you want to transform the submitted + # data before it's persisted. For example, the following would turn all + # empty values into nil values. It uses other APIs such as `resource_class` + # and `dashboard`: + # + # def resource_params + # params.require(resource_class.model_name.param_key). + # permit(dashboard.permitted_attributes(action_name)). + # transform_values { |value| value == "" ? nil : value } + # end + + # See https://administrate-demo.herokuapp.com/customizing_controller_actions + # for more information + end + end +end diff --git a/app/controllers/admin/warehouse/purpose_codes_controller.rb b/app/controllers/admin/warehouse/purpose_codes_controller.rb new file mode 100644 index 0000000..027a5a5 --- /dev/null +++ b/app/controllers/admin/warehouse/purpose_codes_controller.rb @@ -0,0 +1,48 @@ +module Admin + module Warehouse + class PurposeCodesController < Admin::ApplicationController + # Overwrite any of the RESTful controller actions to implement custom behavior + # For example, you may want to send an email after a foo is updated. + # + # def update + # super + # send_foo_updated_email(requested_resource) + # end + + # Override this method to specify custom lookup behavior. + # This will be used to set the resource for the `show`, `edit`, and `update` + # actions. + # + # def find_resource(param) + # Foo.find_by!(slug: param) + # end + + # The result of this lookup will be available as `requested_resource` + + # Override this if you have certain roles that require a subset + # this will be used to set the records shown on the `index` action. + # + # def scoped_resource + # if current_user.super_admin? + # resource_class + # else + # resource_class.with_less_stuff + # end + # end + + # Override `resource_params` if you want to transform the submitted + # data before it's persisted. For example, the following would turn all + # empty values into nil values. It uses other APIs such as `resource_class` + # and `dashboard`: + # + # def resource_params + # params.require(resource_class.model_name.param_key). + # permit(dashboard.permitted_attributes(action_name)). + # transform_values { |value| value == "" ? nil : value } + # end + + # See https://administrate-demo.herokuapp.com/customizing_controller_actions + # for more information + end + end +end diff --git a/app/controllers/admin/warehouse/skus_controller.rb b/app/controllers/admin/warehouse/skus_controller.rb new file mode 100644 index 0000000..418f1da --- /dev/null +++ b/app/controllers/admin/warehouse/skus_controller.rb @@ -0,0 +1,48 @@ +module Admin + module Warehouse + class SKUsController < Admin::ApplicationController + # Overwrite any of the RESTful controller actions to implement custom behavior + # For example, you may want to send an email after a foo is updated. + # + # def update + # super + # send_foo_updated_email(requested_resource) + # end + + # Override this method to specify custom lookup behavior. + # This will be used to set the resource for the `show`, `edit`, and `update` + # actions. + # + # def find_resource(param) + # Foo.find_by!(slug: param) + # end + + # The result of this lookup will be available as `requested_resource` + + # Override this if you have certain roles that require a subset + # this will be used to set the records shown on the `index` action. + # + # def scoped_resource + # if current_user.super_admin? + # resource_class + # else + # resource_class.with_less_stuff + # end + # end + + # Override `resource_params` if you want to transform the submitted + # data before it's persisted. For example, the following would turn all + # empty values into nil values. It uses other APIs such as `resource_class` + # and `dashboard`: + # + # def resource_params + # params.require(resource_class.model_name.param_key). + # permit(dashboard.permitted_attributes(action_name)). + # transform_values { |value| value == "" ? nil : value } + # end + + # See https://administrate-demo.herokuapp.com/customizing_controller_actions + # for more information + end + end +end diff --git a/app/controllers/admin/warehouse/templates_controller.rb b/app/controllers/admin/warehouse/templates_controller.rb new file mode 100644 index 0000000..c067018 --- /dev/null +++ b/app/controllers/admin/warehouse/templates_controller.rb @@ -0,0 +1,48 @@ +module Admin + module Warehouse + class TemplatesController < Admin::ApplicationController + # Overwrite any of the RESTful controller actions to implement custom behavior + # For example, you may want to send an email after a foo is updated. + # + # def update + # super + # send_foo_updated_email(requested_resource) + # end + + # Override this method to specify custom lookup behavior. + # This will be used to set the resource for the `show`, `edit`, and `update` + # actions. + # + def find_resource(param) + Warehouse::Template.find(param) + end + + # The result of this lookup will be available as `requested_resource` + + # Override this if you have certain roles that require a subset + # this will be used to set the records shown on the `index` action. + # + # def scoped_resource + # if current_user.super_admin? + # resource_class + # else + # resource_class.with_less_stuff + # end + # end + + # Override `resource_params` if you want to transform the submitted + # data before it's persisted. For example, the following would turn all + # empty values into nil values. It uses other APIs such as `resource_class` + # and `dashboard`: + # + # def resource_params + # params.require(resource_class.model_name.param_key). + # permit(dashboard.permitted_attributes(action_name)). + # transform_values { |value| value == "" ? nil : value } + # end + + # See https://administrate-demo.herokuapp.com/customizing_controller_actions + # for more information + end + end +end diff --git a/app/controllers/api/v1/application_controller.rb b/app/controllers/api/v1/application_controller.rb new file mode 100644 index 0000000..d1e5b91 --- /dev/null +++ b/app/controllers/api/v1/application_controller.rb @@ -0,0 +1,59 @@ +module API + module V1 + class ApplicationController < ActionController::API + prepend_view_path Rails.root.join("app/views/api/v1") + attr_reader :current_user + + before_action :authenticate! + before_action :set_expand + before_action :set_pii + + include Pundit::Authorization + include ActionController::HttpAuthentication::Token::ControllerMethods + + rescue_from Pundit::NotAuthorizedError do |e| + render json: { error: "not_authorized" }, status: :forbidden + end + + rescue_from ActiveRecord::RecordNotFound do |e| + render json: { error: "resource_not_found", message: ("Couldn't locate that #{e.model.constantize.model_name.human}." if e.model) }.compact_blank, status: :not_found + end + + rescue_from ActiveRecord::RecordInvalid do |e| + render json: { error: "validation_error", messages: e.record.errors.full_messages }, status: :bad_request + end + + rescue_from ActiveRecord::RecordNotUnique do |e| + render json: { error: "idempotency_error", messages: ["a record by that idempotency key already exists!"] }, status: :bad_request + end + + private + + def set_expand + @expand = params[:expand].to_s.split(",").map { |e| e.strip.to_sym } + end + + def set_pii + @pii = current_token&.pii? + end + + def authenticate! + @current_token = authenticate_with_http_token { |t, _options| APIKey.find_by(token: t) } + unless @current_token&.active? + return render json: { error: "invalid_auth" }, status: :unauthorized + end + @current_user = if current_token&.may_impersonate? && params[:impersonate].present? + begin + User.find_by!(slack_id: params[:impersonate]) + rescue ActiveRecord::RecordNotFound + render json: { error: "impersonate_error", message: "couldn't find that user" }, status: :bad_request + end + else + current_token&.user + end + end + + attr_reader :current_token + end + end +end diff --git a/app/controllers/api/v1/letter_queues_controller.rb b/app/controllers/api/v1/letter_queues_controller.rb new file mode 100644 index 0000000..dfe8360 --- /dev/null +++ b/app/controllers/api/v1/letter_queues_controller.rb @@ -0,0 +1,128 @@ +module API + module V1 + class LetterQueuesController < ApplicationController + before_action :set_letter_queue, only: [:show, :create_letter] + before_action :set_instant_letter_queue, only: [:create_instant_letter, :queued] + + rescue_from ActiveRecord::RecordNotFound do |e| + render json: { error: "Queue not found" }, status: :not_found + end + + rescue_from ActiveRecord::RecordInvalid do |e| + Honeybadger.notify(e) + render json: { + error: "Validation failed", + details: e.record.errors.full_messages, + }, status: :unprocessable_entity + end + + def show + authorize @letter_queue + end + + # this should REALLY be websockets or some shit + # but it's not, so we're just going to poll + # i'm not braining well enough to do it right anytime soon + def queued + # authorize @letter_queue, policy_class: Letter::QueuePolicy + raise Pundit::NotAuthorizedError unless current_token&.pii? + + return render json: { error: "no" } unless @letter_queue.is_a?(Letter::InstantQueue) + @expand = [:label] + + @letters = @letter_queue.letters.pending + end + + def create_letter + authorize @letter_queue + + # Normalize country name using FrickinCountryNames + country = FrickinCountryNames.find_country(letter_params[:address][:country]) + if country.nil? + render json: { error: "couldn't figure out country name #{letter_params[:address][:country]}" }, status: :unprocessable_entity + return + end + + # Create address with normalized country + address_params = letter_params[:address].merge(country: country.alpha2) + # Normalize state name to abbreviation + address_params[:state] = FrickinCountryNames.normalize_state(country, address_params[:state]) + addy = Address.new(address_params) + + @letter = @letter_queue.create_letter!( + addy, + letter_params.except(:address).merge(user: current_user), + ) + render :create_letter, status: :created + + # rescue ActiveRecord::RecordInvalid => e + # render json: { error: e.record.errors.full_messages }, status: :unprocessable_entity + end + + def create_instant_letter + authorize @letter_queue, policy_class: Letter::QueuePolicy + + # Normalize country name using FrickinCountryNames + country = FrickinCountryNames.find_country(letter_params[:address][:country]) + if country.nil? + render json: { error: "couldn't figure out country name #{letter_params[:address][:country]}" }, status: :unprocessable_entity + return + end + + # Create address with normalized country + address_params = letter_params[:address].merge(country: country.alpha2) + # Normalize state name to abbreviation + address_params[:state] = FrickinCountryNames.normalize_state(country, address_params[:state]) + addy = Address.new(address_params) + + @letter = @letter_queue.process_letter_instantly!( + addy, + letter_params.except(:address).merge(user: current_user), + ) + @expand << :label + render :create_letter, status: :created + rescue ActiveRecord::RecordNotFound + return render json: { error: "Queue not found" }, status: :not_found + rescue ActiveRecord::RecordInvalid => e + return render json: { error: e.record.errors.full_messages.join(", ") }, status: :unprocessable_entity + end + + private + + def set_letter_queue + @letter_queue = Letter::Queue.find_by!(slug: params[:id]) + # grossest hack on the planet, nora why are you like this + raise ActiveRecord::RecordNotFound if @letter_queue.is_a?(Letter::InstantQueue) + end + + def set_instant_letter_queue + @letter_queue = Letter::InstantQueue.find_by!(slug: params[:id]) + end + + def letter_params + params.permit( + :rubber_stamps, + :recipient_email, + :idempotency_key, + :return_address_name, + metadata: {}, + address: [ + :first_name, + :last_name, + :line_1, + :line_2, + :city, + :state, + :postal_code, + :country, + ], + ) + end + + def normalize_country(country) + return "US" if country.blank? || country.downcase == "usa" || country.downcase == "united states" + country + end + end + end +end diff --git a/app/controllers/api/v1/letters_controller.rb b/app/controllers/api/v1/letters_controller.rb new file mode 100644 index 0000000..62c22f1 --- /dev/null +++ b/app/controllers/api/v1/letters_controller.rb @@ -0,0 +1,29 @@ +module API + module V1 + class LettersController < ApplicationController + before_action :set_letter, only: [:show, :mark_printed] + + def show + authorize @letter + end + + def by_tag + @letters = Letter.where("? = ANY(tags)", params[:tag]) + authorize @letters + render :letters_collection + end + + def mark_printed + authorize @letter + @letter.mark_printed! + render json: { message: "Letter marked as printed" } + end + + private + + def set_letter + @letter = Letter.find_by_public_id!(params[:id]) + end + end + end +end diff --git a/app/controllers/api/v1/qz_trays_controller.rb b/app/controllers/api/v1/qz_trays_controller.rb new file mode 100644 index 0000000..baf8e78 --- /dev/null +++ b/app/controllers/api/v1/qz_trays_controller.rb @@ -0,0 +1,13 @@ +module API + module V1 + class QZTraysController < ApplicationController + def cert + send_data QZTrayService.certificate + end + + def sign + send_data QZTrayService.sign(params.require(:request)) + end + end + end +end diff --git a/app/controllers/api/v1/tags_controller.rb b/app/controllers/api/v1/tags_controller.rb new file mode 100644 index 0000000..f2255c1 --- /dev/null +++ b/app/controllers/api/v1/tags_controller.rb @@ -0,0 +1,50 @@ +module API + module V1 + class TagsController < ApplicationController + # skip_before_action :authenticate! + + def index + @tags = ::ApplicationController.helpers.available_tags + end + + def show + @tag = params[:id] + + letter_query = Letter.with_any_tags([@tag]).where(aasm_state: [:mailed, :received]) + wh_order_query = Warehouse::Order.with_any_tags([@tag]).where.not(aasm_state: [:draft, :errored]) + + if letter_query.none? && wh_order_query.none? + render json: { error: "no letters or warehouse orders found for tag #{@tag}...?" }, status: :not_found + return + end + + cache_key = "api/v1/tags/#{@tag}" + cache_options = params[:no_cache] ? { force: true } : { expires_in: 5.minutes } + + cached_data = Rails.cache.fetch(cache_key, cache_options) do + { + letter_count: letter_query.count, + letter_postage_cost: letter_query.sum(:postage), + warehouse_order_count: wh_order_query.count, + warehouse_order_postage_cost: wh_order_query.sum(:postage_cost), + warehouse_order_labor_cost: wh_order_query.sum(:labor_cost), + warehouse_order_contents_cost: wh_order_query.sum(:contents_cost), + warehouse_order_total_cost: wh_order_query.sum(:postage_cost) + wh_order_query.sum(:labor_cost) + wh_order_query.sum(:contents_cost), + } + end + + @letter_count = cached_data[:letter_count] + @letter_postage_cost = cached_data[:letter_postage_cost] + @warehouse_order_count = cached_data[:warehouse_order_count] + @warehouse_order_postage_cost = cached_data[:warehouse_order_postage_cost] + @warehouse_order_labor_cost = cached_data[:warehouse_order_labor_cost] + @warehouse_order_contents_cost = cached_data[:warehouse_order_contents_cost] + @warehouse_order_total_cost = cached_data[:warehouse_order_total_cost] + end + + def letters + @letters = Letter.with_any_tags(params[:id]) + end + end + end +end diff --git a/app/controllers/api/v1/users_controller.rb b/app/controllers/api/v1/users_controller.rb new file mode 100644 index 0000000..66a0c4d --- /dev/null +++ b/app/controllers/api/v1/users_controller.rb @@ -0,0 +1,10 @@ +module API + module V1 + class UsersController < ApplicationController + def show + @user = authorize current_user + end + + end + end +end \ No newline at end of file diff --git a/app/controllers/api_keys_controller.rb b/app/controllers/api_keys_controller.rb new file mode 100644 index 0000000..ad65e83 --- /dev/null +++ b/app/controllers/api_keys_controller.rb @@ -0,0 +1,50 @@ +class APIKeysController < ApplicationController + before_action :set_api_key, except: [:index, :new, :create] + + def index + authorize APIKey + @api_keys = policy_scope(APIKey) + end + + def new + authorize APIKey + @api_key = APIKey.new(user: current_user) + end + + def create + permitted_params = [:name, :pii] + permitted_params << :may_impersonate if current_user.admin? + + @api_key = APIKey.new(params.require(:api_key).permit(*permitted_params).merge(user: current_user)) + + authorize @api_key + + if @api_key.save + redirect_to api_key_path(@api_key) + else + flash[:error] = @api_key.errors.full_messages.to_sentence + redirect_to new_api_key_path(@api_key) + end + end + + def show + authorize @api_key + end + + def revoke_confirm + authorize @api_key + end + + def revoke + authorize @api_key + @api_key.revoke! + flash[:success] = "terminated with extreme prejudice." + redirect_to api_key_path(@api_key) + end + + private + + def set_api_key + @api_key = policy_scope(APIKey).find(params[:id]) + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 0000000..84d50ac --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,39 @@ +class ApplicationController < ActionController::Base + include Pundit::Authorization + after_action :verify_authorized + + helper_method :current_user, :user_signed_in? + + before_action :authenticate_user!, :set_honeybadger_context + + def current_user + @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id] + end + + def user_signed_in? + !!current_user + end + + def authenticate_user! + unless user_signed_in? + redirect_to login_path, alert: ("you need to be logged in!" unless request.env["PATH_INFO"] == "/back_office") + end + end + + def set_honeybadger_context + Honeybadger.context({ + user_id: current_user&.id, + user_email: current_user&.email, + }) + end + + rescue_from Pundit::NotAuthorizedError do |e| + flash[:error] = "you don't seem to be authorized – ask nora?" + redirect_to root_path + end + + rescue_from ActiveRecord::RecordNotFound do |e| + flash[:error] = "sorry, couldn't find that object... (404)" + redirect_to root_path + end +end diff --git a/app/controllers/base_batches_controller.rb b/app/controllers/base_batches_controller.rb new file mode 100644 index 0000000..8c41170 --- /dev/null +++ b/app/controllers/base_batches_controller.rb @@ -0,0 +1,111 @@ +class BaseBatchesController < ApplicationController + before_action :set_batch, except: %i[ index new create ] + before_action :setup_csv_fields, only: %i[ map_fields set_mapping ] + + REQUIRED_FIELDS = %w[first_name line_1 city state postal_code country].freeze + PREVIEW_ROWS = 3 + + # GET /batches or /batches.json + def index + authorize Batch + @batches = policy_scope(Batch).order(created_at: :desc) + end + + # GET /batches/1 or /batches/1.json + def show + authorize @batch + end + + # GET /batches/1/edit + def edit + authorize @batch + end + + # PATCH/PUT /batches/1 or /batches/1.json + def update + authorize @batch + respond_to do |format| + if @batch.update(batch_params) + format.html { redirect_to @batch, notice: "Batch was successfully updated." } + format.json { render :show, status: :ok, location: @batch } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @batch.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /batches/1 or /batches/1.json + def destroy + authorize @batch + @batch.destroy! + + respond_to do |format| + format.html { redirect_to batches_path, status: :see_other, notice: "Batch was successfully destroyed." } + format.json { head :no_content } + end + end + + def map_fields + authorize @batch, :map_fields? + end + + def set_mapping + authorize @batch, :set_mapping? + mapping = mapping_params.to_h + + # Invert the mapping to get from CSV columns to address fields + inverted_mapping = mapping.invert + + # Validate required fields + missing_fields = REQUIRED_FIELDS.reject { |field| inverted_mapping[field].present? } + + if missing_fields.any? + flash.now[:error] = "Please map the following required fields: #{missing_fields.join(", ")}" + render :map_fields, status: :unprocessable_entity + return + end + + if @batch.update!(field_mapping: inverted_mapping) + begin + @batch.run_map! + rescue StandardError => e + raise + Rails.logger.warn(e) + uuid = Honeybadger.notify(e) + redirect_to send("#{@batch.class.name.split("::").first.downcase}_batch_path", @batch), flash: { alert: "error mapping fields! #{e.message} (please report EID: #{uuid})" } + return + end + redirect_to send("process_confirm_#{@batch.class.name.split("::").first.downcase}_batch_path", @batch), notice: "Field mapping saved. Please review and process your batch." + else + flash.now[:error] = "failed to save field mapping. #{@batch.errors.full_messages.join(", ")}" + render :map_fields, status: :unprocessable_entity + end + end + + private + + def set_batch + @batch = Batch.find(params[:id]) + end + + def setup_csv_fields + csv_rows = CSV.parse(@batch.csv_data) + @csv_headers = csv_rows.first + @csv_preview = csv_rows[1..PREVIEW_ROWS] || [] + + # Get fields based on batch type + @address_fields = if @batch.is_a?(Letter::Batch) + # For letter batches, include address fields and rubber_stamps + (Address.column_names - ["id", "created_at", "updated_at", "batch_id"]) + + ["rubber_stamps"] + else + # For other batches, just include address fields + (Address.column_names - ["id", "created_at", "updated_at"]) + end + end + + def mapping_params + params.require(:mapping).permit! + end +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/concerns/administrate/array_parameter_handler.rb b/app/controllers/concerns/administrate/array_parameter_handler.rb new file mode 100644 index 0000000..5b1c601 --- /dev/null +++ b/app/controllers/concerns/administrate/array_parameter_handler.rb @@ -0,0 +1,21 @@ +module Administrate + module ArrayParameterHandler + extend ActiveSupport::Concern + + included do + before_action :handle_array_parameters + end + + private + + def handle_array_parameters + return unless params[:warehouse_order].present? + + params[:warehouse_order].each do |key, value| + if value.is_a?(String) && value.include?(",") + params[:warehouse_order][key] = value.split(",").map(&:strip) + end + end + end + end +end \ No newline at end of file diff --git a/app/controllers/concerns/public/frameable.rb b/app/controllers/concerns/public/frameable.rb new file mode 100644 index 0000000..7471452 --- /dev/null +++ b/app/controllers/concerns/public/frameable.rb @@ -0,0 +1,30 @@ +module Public::Frameable + extend ActiveSupport::Concern + included do + layout "public/frameable" + + def set_framed + @framed = request.headers["Sec-Fetch-Dest"] == "iframe" + end + + before_action :set_framed + + def frame_aware_redirect_to(path, **options) + if @framed + redirect_to "#{path}#{path.include?('?') ? '&' : '?'}framed=true", **options + else + redirect_to path, **options + end + end + + private + + def add_framed_param(path) + uri = URI.parse(path) + params = URI.decode_www_form(uri.query || '') + params << ['framed', @framed] + uri.query = URI.encode_www_form(params) + uri.to_s + end + end +end \ No newline at end of file diff --git a/app/controllers/customs_receipts_controller.rb b/app/controllers/customs_receipts_controller.rb new file mode 100644 index 0000000..e054574 --- /dev/null +++ b/app/controllers/customs_receipts_controller.rb @@ -0,0 +1,37 @@ +class CustomsReceiptsController < ApplicationController + def index + authorize :customs_receipt + end + + def generate + authorize :customs_receipt + + return redirect_to customs_receipts_path, alert: "well, ya gotta search for *something*" if params[:search].blank? + + receiptable = get_receiptable(params[:search]) + + if receiptable.nil? + flash[:error] = "couldn't find anything about #{params[:search]}... better luck next time?" + return redirect_to customs_receipts_path + end + + send_data CustomsReceipt::Generate.run(receiptable), filename: "customs_receipt.pdf", type: "application/pdf", disposition: "inline" + end + + private + + def get_receiptable(search) + order = Warehouse::Order.where("hc_id = :search or tracking_number = :search", search:).first + return CustomsReceipt::TheseusSpecific.receiptable_from_warehouse_order(order) if order + + sanitized_airtable_search = search.gsub("'", "\\'") + + msr = LSV::MarketingShipmentRequest.first_where( + "OR({Airtable ID (Automation)} = '#{sanitized_airtable_search}', {Warehouse–Tracking Number} = '#{sanitized_airtable_search}')" + ) + + return CustomsReceipt::TheseusSpecific.receiptable_from_msr(msr) if msr + + nil + end +end diff --git a/app/controllers/inspect/indicia_controller.rb b/app/controllers/inspect/indicia_controller.rb new file mode 100644 index 0000000..9c0548c --- /dev/null +++ b/app/controllers/inspect/indicia_controller.rb @@ -0,0 +1,6 @@ +module Inspect + class IndiciaController < InspectorController + MODEL = USPS::Indicium + LINKED_FIELDS = %i(letter) + end +end diff --git a/app/controllers/inspect/inspector_controller.rb b/app/controllers/inspect/inspector_controller.rb new file mode 100644 index 0000000..185aa03 --- /dev/null +++ b/app/controllers/inspect/inspector_controller.rb @@ -0,0 +1,28 @@ +module Inspect + class InspectorController < ApplicationController + skip_after_action :verify_authorized + before_action :set_record + before_action do + unless current_user.admin? + redirect_to root_path, alert: "you are not authorized to access this page." + end + end + + def show + @linked_fields = self.class::LINKED_FIELDS.each_with_object({}) do |field, hash| + hash[field] = nil + res = @record.send(field) + next if res.blank? + hash[field] = url_for(res) if res.present? + rescue ActionController::RoutingError + end + render "inspect/show" + end + + private + + def set_record + @record = self.class::MODEL.find_by_public_id(params[:id]) || self.class::MODEL.find(params[:id]) + end + end +end diff --git a/app/controllers/inspect/iv_mtr_events_controller.rb b/app/controllers/inspect/iv_mtr_events_controller.rb new file mode 100644 index 0000000..6f1f4fd --- /dev/null +++ b/app/controllers/inspect/iv_mtr_events_controller.rb @@ -0,0 +1,8 @@ +module Inspect + class IVMTREventsController < InspectorController + MODEL = USPS::IVMTR::Event + LINKED_FIELDS = %i(letter) + + private + end +end diff --git a/app/controllers/letter/batches_controller.rb b/app/controllers/letter/batches_controller.rb new file mode 100644 index 0000000..02e4f10 --- /dev/null +++ b/app/controllers/letter/batches_controller.rb @@ -0,0 +1,240 @@ +class Letter::BatchesController < BaseBatchesController + # GET /letter/batches + def index + authorize Letter::Batch, policy_class: Letter::BatchPolicy + @batches = policy_scope(Letter::Batch, policy_scope_class: Letter::BatchPolicy::Scope).order(created_at: :desc) + end + + # GET /letter/batches/new + def new + authorize Letter::Batch, policy_class: Letter::BatchPolicy + @batch = Letter::Batch.new + end + + # GET /letter/batches/:id + def show + authorize @batch, policy_class: Letter::BatchPolicy + end + + # GET /letter/batches/:id/edit + def edit + authorize @batch, policy_class: Letter::BatchPolicy + end + + # POST /letter/batches + def create + authorize Letter::Batch, policy_class: Letter::BatchPolicy + @batch = Letter::Batch.new(batch_params.merge(user: current_user)) + + if @batch.save + redirect_to map_fields_letter_batch_path(@batch), notice: "Batch was successfully created." + else + render :new, status: :unprocessable_entity + end + end + + # PATCH /letter/batches/:id + def update + authorize @batch, policy_class: Letter::BatchPolicy + if @batch.update(batch_params) + validate_postage_types + if @batch.errors.any? + render :edit, status: :unprocessable_entity + return + end + + # Update associated letters if the batch hasn't been processed + if @batch.may_mark_processed? + @batch.letters.update_all( + height: @batch.letter_height, + width: @batch.letter_width, + weight: @batch.letter_weight, + mailing_date: @batch.letter_mailing_date, + usps_mailer_id_id: @batch.letter_mailer_id_id, + return_address_id: @batch.letter_return_address_id, + return_address_name: @batch.letter_return_address_name, + ) + end + + # Always update tags and user facing title on letters + @batch.letters.update_all( + tags: @batch.tags, + user_facing_title: @batch.user_facing_title, + ) + + redirect_to letter_batch_path(@batch), notice: "Batch was successfully updated." + else + render :edit, status: :unprocessable_entity + end + end + + # DELETE /letter/batches/:id + def destroy + authorize @batch, policy_class: Letter::BatchPolicy + @batch.destroy + redirect_to letter_batches_path, notice: "Batch was successfully destroyed." + end + + def process_form + authorize @batch, :process_form?, policy_class: Letter::BatchPolicy + render :process_letter + end + + def process_batch + authorize @batch, :process_batch?, policy_class: Letter::BatchPolicy + @batch = Batch.find(params[:id]) + + if request.post? + if letter_batch_params[:letter_mailing_date].blank? + redirect_to process_letter_batch_path(@batch), alert: "Mailing date is required" + return + end + + @batch.letter_mailing_date = letter_batch_params[:letter_mailing_date] + @batch.save! # Save the mailing date before processing + + # Only require payment account if indicia is selected + if letter_batch_params[:us_postage_type] == "indicia" || letter_batch_params[:intl_postage_type] == "indicia" + payment_account = USPS::PaymentAccount.find_by(id: letter_batch_params[:usps_payment_account_id]) + + if payment_account.nil? + redirect_to process_letter_batch_path(@batch), alert: "Please select a valid payment account when using indicia" + return + end + end + + begin + @batch.process!( + payment_account: payment_account, + us_postage_type: letter_batch_params[:us_postage_type], + intl_postage_type: letter_batch_params[:intl_postage_type], + template_cycle: letter_batch_params[:template_cycle].to_s.split(",").compact_blank, + user_facing_title: letter_batch_params[:user_facing_title], + include_qr_code: letter_batch_params[:include_qr_code], + ) + @batch.mark_processed! if @batch.may_mark_processed? + + redirect_to letter_batch_path(@batch, print_now: letter_batch_params[:print_immediately]), notice: "Batch processed successfully" + rescue => e + uuid = Honeybadger.notify(e) + redirect_to process_letter_batch_path(@batch), alert: "Failed to process batch: #{e.message} (please report EID: #{uuid})" + end + end + end + + def mark_printed + authorize @batch, :mark_printed?, policy_class: Letter::BatchPolicy + if @batch.processed? + @batch.letters.each do |letter| + letter.mark_printed! if letter.may_mark_printed? + end + flash[:success] = "all letters have been marked as printed!" + redirect_to letter_batch_path(@batch) + else + flash[:alert] = "Cannot mark letters as printed. Batch must be processed." + redirect_to letter_batch_path(@batch) + end + end + + def mark_mailed + authorize @batch, :mark_mailed?, policy_class: Letter::BatchPolicy + if @batch.processed? + @batch.letters.each do |letter| + letter.mark_mailed! if letter.may_mark_mailed? + end + User::UpdateTasksJob.perform_now(current_user) + redirect_to letter_batch_path(@batch), notice: "All letters have been marked as mailed." + else + redirect_to letter_batch_path(@batch), alert: "Cannot mark letters as mailed. Batch must be processed." + end + end + + def update_costs + authorize @batch, :update_costs?, policy_class: Letter::BatchPolicy + # Calculate counts without saving + us_letters = @batch.letters.joins(:address).where(addresses: { country: "US" }) + intl_letters = @batch.letters.joins(:address).where.not(addresses: { country: "US" }) + + cost_differences = @batch.postage_cost_difference( + us_postage_type: params[:us_postage_type], + intl_postage_type: params[:intl_postage_type], + ) + + render json: { + total_cost: @batch.postage_cost, + cost_difference: { + us: cost_differences[:us], + intl: cost_differences[:intl], + }, + us_count: us_letters.count, + intl_count: intl_letters.count, + } + end + + def regenerate_form + authorize @batch, :process_batch?, policy_class: Letter::BatchPolicy + render :regenerate_labels + end + + def regenerate_labels + authorize @batch, :process_batch?, policy_class: Letter::BatchPolicy + @batch.regenerate_labels!( + template_cycle: letter_batch_params[:template_cycle].to_s.split(",").compact_blank, + include_qr_code: letter_batch_params[:include_qr_code], + ) + redirect_to letter_batch_path(@batch), notice: "Labels regenerated successfully" + end + + private + + def batch_params + params.require(:letter_batch).permit( + :csv, + :letter_template_id, + :user_facing_title, + :letter_height, + :letter_width, + :letter_weight, + :letter_mailing_date, + :letter_mailer_id_id, + :letter_return_address_id, + :letter_return_address_name, + :letter_processing_category, + tags: [], + ) + end + + def letter_batch_params + params.require(:batch).permit( + :csv, + :letter_height, + :letter_width, + :user_facing_title, + :letter_weight, + :letter_mailing_date, + :letter_processing_category, + :letter_mailer_id_id, + :letter_return_address_id, + :letter_return_address_name, + :us_postage_type, + :intl_postage_type, + :usps_payment_account_id, + :include_qr_code, + :print_immediately, + :template_cycle, + tags: [], + ) + end + + def validate_postage_types + return unless @batch.letter_return_address&.us? + + if @batch.us_postage_type.present? && !%w[stamps indicia].include?(@batch.us_postage_type) + @batch.errors.add(:us_postage_type, "must be either 'stamps' or 'indicia'") + end + + if @batch.intl_postage_type.present? && !%w[stamps indicia].include?(@batch.intl_postage_type) + @batch.errors.add(:intl_postage_type, "must be either 'stamps' or 'indicia'") + end + end +end diff --git a/app/controllers/letter/instant_queues_controller.rb b/app/controllers/letter/instant_queues_controller.rb new file mode 100644 index 0000000..7268600 --- /dev/null +++ b/app/controllers/letter/instant_queues_controller.rb @@ -0,0 +1,38 @@ +class Letter::InstantQueuesController < Letter::QueuesController + before_action :set_letter_queue, only: %i[ show edit update destroy ] + + def new + @letter_queue = Letter::InstantQueue.new + end + + def show + @letters = @letter_queue.letters + end + + private + + def set_letter_queue + @letter_queue = Letter::InstantQueue.find_by!(slug: params[:id]) + end + + def letter_queue_params + params.require(:letter_instant_queue).permit( + :name, + :type, + :letter_height, + :letter_width, + :letter_weight, + :letter_processing_category, + :letter_mailer_id_id, + :letter_return_address_id, + :letter_return_address_name, + :user_facing_title, + :template, + :postage_type, + :usps_payment_account_id, + :include_qr_code, + :letter_mailing_date, + tags: [], + ) + end +end diff --git a/app/controllers/letter/queues_controller.rb b/app/controllers/letter/queues_controller.rb new file mode 100644 index 0000000..8e08e73 --- /dev/null +++ b/app/controllers/letter/queues_controller.rb @@ -0,0 +1,152 @@ +class Letter::QueuesController < ApplicationController + before_action :set_letter_queue, only: %i[ show edit update destroy batch ] + skip_after_action :verify_authorized + # GET /letter/queues or /letter/queues.json + def index + authorize Letter::Queue, policy_class: Letter::QueuePolicy + @letter_queues = policy_scope(Letter::Queue, policy_scope_class: Letter::QueuePolicy::Scope) + end + + # GET /letter/queues/1 or /letter/queues/1.json + def show + @letters = @letter_queue.letters.queued + @batches = @letter_queue.letter_batches + end + + # GET /letter/queues/new + def new + @letter_queue = Letter::Queue.new + end + + # GET /letter/queues/1/edit + def edit + end + + # POST /letter/queues or /letter/queues.json + def create + @letter_queue = letter_queue_class.new(letter_queue_params.merge(user: current_user)) + + respond_to do |format| + if @letter_queue.save + format.html { redirect_to @letter_queue, notice: "Queue was successfully created." } + format.json { render :show, status: :created, location: @letter_queue } + else + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @letter_queue.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /letter/queues/1 or /letter/queues/1.json + def update + respond_to do |format| + if @letter_queue.update(letter_queue_params) + format.html { redirect_to @letter_queue, notice: "Queue was successfully updated." } + format.json { render :show, status: :ok, location: @letter_queue } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @letter_queue.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /letter/queues/1 or /letter/queues/1.json + def destroy + @letter_queue.destroy! + + respond_to do |format| + format.html { redirect_to letter_queues_path, status: :see_other, notice: "Queue was successfully destroyed." } + format.json { head :no_content } + end + end + + def batch + authorize @letter_queue + unless @letter_queue.letters.any? + flash[:error] = "no letters?" + redirect_to @letter_queue + end + batch = @letter_queue.make_batch(user: current_user) + User::UpdateTasksJob.perform_now(current_user) + flash[:success] = "now do something with it!" + redirect_to process_letter_batch_path(batch, uft: @letter_queue.user_facing_title, template: @letter_queue.template) + end + + def mark_printed_instants_mailed + authorize Letter::Queue + + # Find all letters with "printed" status in any instant letter queue + printed_letters = Letter.joins(:queue) + .where(letter_queues: { type: "Letter::InstantQueue" }) + .where(aasm_state: "printed") + + if printed_letters.empty? + flash[:notice] = "No printed letters found in instant queues." + redirect_to letter_queues_path + return + end + + # Mark all printed letters as mailed + marked_count = 0 + failed_letters = [] + + printed_letters.each do |letter| + begin + letter.mark_mailed! + marked_count += 1 + rescue => e + failed_letters << "#{letter.public_id} (#{e.message})" + end + end + + # Update user tasks after marking letters as mailed + User::UpdateTasksJob.perform_now(current_user) if marked_count > 0 + + if failed_letters.any? + flash[:alert] = "Marked #{marked_count} letters as mailed, but failed to mark #{failed_letters.count} letters: #{failed_letters.join(", ")}" + else + flash[:success] = "Successfully marked #{marked_count} letters as mailed from instant queues." + end + + redirect_to letter_queues_path + end + + private + + # Use callbacks to share common setup or constraints between actions. + def set_letter_queue + @letter_queue = Letter::Queue.find_by!(slug: params[:id]) + end + + def letter_queue_class + type = params[:letter_queue]&.dig(:type) || params[:letter_instant_queue]&.dig(:type) + case type + when "Letter::InstantQueue" + Letter::InstantQueue + else + Letter::Queue + end + end + + # Only allow a list of trusted parameters through. + def letter_queue_params + params.require(:letter_queue).permit( + :name, + :type, + :letter_height, + :letter_width, + :letter_weight, + :letter_processing_category, + :letter_mailer_id_id, + :letter_return_address_id, + :letter_return_address_name, + :user_facing_title, + :template, + :postage_type, + :usps_payment_account_id, + :include_qr_code, + :letter_mailing_date, + tags: [], + ) + end +end diff --git a/app/controllers/letters_controller.rb b/app/controllers/letters_controller.rb new file mode 100644 index 0000000..95ed41a --- /dev/null +++ b/app/controllers/letters_controller.rb @@ -0,0 +1,243 @@ +class LettersController < ApplicationController + before_action :set_letter, except: %i[ index new create ] + + # GET /letters + def index + authorize Letter + # Get all letters with their associations using policy scope + @all_letters = policy_scope(Letter).includes(:batch, :address, :usps_mailer_id, :label_attachment, :label_blob) + .where.not(aasm_state: "queued") + .order(created_at: :desc) + + # Get unbatched letters with pagination + @unbatched_letters = @all_letters.not_in_batch.page(params[:page]).per(20) + + # Get batched letters grouped by batch + @batched_letters = @all_letters.in_batch.group_by(&:batch) + end + + # GET /letters/1 + def show + authorize @letter + @available_templates = SnailMail::Service.available_templates + end + + # GET /letters/new + def new + authorize Letter + @letter = Letter.new + @letter.return_address = current_user.home_return_address || ReturnAddress.first + @letter.build_address + end + + # GET /letters/1/edit + def edit + authorize @letter + # If letter doesn't have a return address already, don't build one + # Let the user select one from the dropdown + end + + # POST /letters + def create + @letter = Letter.new(letter_params.merge(user: current_user)) + authorize @letter + + # Set postage type to international_origin if return address is not US + if @letter.return_address && @letter.return_address.country != "US" + @letter.postage_type = "international_origin" + end + + if @letter.save + redirect_to @letter, notice: "Letter was successfully created." + else + render :new, status: :unprocessable_entity + end + end + + # PATCH/PUT /letters/1 + def update + authorize @letter + + if @letter.batch_id.present? && params[:letter][:postage_type].present? + redirect_to @letter, alert: "Cannot change postage type for a letter that is part of a batch." + return + end + + # Set postage type to international_origin if return address is not US + if params[:letter][:return_address_id].present? + return_address = ReturnAddress.find(params[:letter][:return_address_id]) + if return_address.country != "US" + params[:letter][:postage_type] = "international_origin" + end + end + + if @letter.update(letter_params) + redirect_to @letter, notice: "Letter was successfully updated." + else + render :edit, status: :unprocessable_entity + end + end + + # DELETE /letters/1 + def destroy + authorize @letter + @letter.destroy! + redirect_to letters_path, status: :see_other, notice: "Letter was successfully destroyed." + end + + # POST /letters/1/generate_label + def generate_label + authorize @letter, :generate_label? + template = params[:template] + include_qr_code = params[:qr].present? + + # Generate label with specified template + begin + # Let the model method handle saving itself + if @letter.generate_label(template:, include_qr_code:) + if @letter.label.attached? + # Redirect back to the letter page with a success message + redirect_to @letter, notice: "Label was successfully generated." + else + redirect_to @letter, alert: "Failed to generate label." + end + else + redirect_to @letter, alert: "Failed to generate label: #{@letter.errors.full_messages.join(", ")}" + end + rescue => e + raise + redirect_to @letter, alert: "Error generating label: #{e.message}" + end + end + + def preview_template + authorize @letter, :preview_template? + template = params["template"] + include_qr_code = params["qr"].present? + send_data SnailMail::Service.generate_label(@letter, { template:, include_qr_code: }).render, type: "application/pdf", disposition: "inline" + end + + # POST /letters/1/mark_printed + def mark_printed + authorize @letter, :mark_printed? + if @letter.mark_printed! + redirect_to @letter, notice: "Letter has been marked as printed." + else + redirect_to @letter, alert: "Could not mark letter as printed: #{@letter.errors.full_messages.join(", ")}" + end + end + + # POST /letters/1/mark_mailed + def mark_mailed + authorize @letter, :mark_mailed? + if @letter.mark_mailed! + User::UpdateTasksJob.perform_now(current_user) + redirect_to @letter, notice: "Letter has been marked as mailed." + else + redirect_to @letter, alert: "Could not mark letter as mailed: #{@letter.errors.full_messages.join(", ")}" + end + end + + # POST /letters/1/mark_received + def mark_received + authorize @letter, :mark_received? + if @letter.mark_received! + redirect_to @letter, notice: "Letter has been marked as received." + else + redirect_to @letter, alert: "Could not mark letter as received: #{@letter.errors.full_messages.join(", ")}" + end + end + + # POST /letters/1/clear_label + def clear_label + authorize @letter, :clear_label? + if @letter.pending? && @letter.label.attached? + @letter.label.purge + redirect_to @letter, notice: "Label has been cleared." + else + redirect_to @letter, alert: "Cannot clear label: Letter is not in pending state or has no label attached." + end + end + + # POST /letters/1/buy_indicia + def buy_indicia + authorize @letter, :buy_indicia? + if @letter.batch_id.present? + redirect_to @letter, alert: "Cannot buy indicia for a letter that is part of a batch." + return + end + + if @letter.postage_type != "indicia" + redirect_to @letter, alert: "Letter must be set to indicia postage type first." + return + end + + if @letter.usps_indicium.present? + redirect_to @letter, alert: "Indicia already purchased for this letter." + return + end + + payment_account = USPS::PaymentAccount.find_by(id: params[:usps_payment_account_id]) + if payment_account.nil? + redirect_to @letter, alert: "Please select a valid payment account." + return + end + + indicium = USPS::Indicium.new(letter: @letter, payment_account: payment_account) + begin + indicium.buy! + redirect_to @letter, notice: "Indicia purchased successfully." + rescue => e + redirect_to @letter, alert: "Failed to purchase indicia: #{e.message}" + end + end + + private + + # Use callbacks to share common setup or constraints between actions. + def set_letter + @letter = Letter.find_by_public_id!(params[:id]) + end + + # Only allow a list of trusted parameters through. + def letter_params + params.require(:letter).permit( + :body, + :height, + :width, + :weight, + :non_machinable, + :processing_category, + :postage_type, + :mailing_date, + :rubber_stamps, + :user_facing_title, + :usps_mailer_id_id, + :return_address_id, + :return_address_name, + :recipient_email, + address_attributes: [ + :id, + :first_name, + :last_name, + :line_1, + :line_2, + :city, + :state, + :postal_code, + :country, + ], + return_address_attributes: [ + :id, + :name, + :line_1, + :line_2, + :city, + :state, + :postal_code, + :country, + ], + tags: [], + ) + end +end diff --git a/app/controllers/public/api/v1/application_controller.rb b/app/controllers/public/api/v1/application_controller.rb new file mode 100644 index 0000000..7a2b3bc --- /dev/null +++ b/app/controllers/public/api/v1/application_controller.rb @@ -0,0 +1,39 @@ +module Public + module API + module V1 + class ApplicationController < ActionController::API + prepend_view_path "app/views/public/api/v1" + + attr_reader :current_public_user + + before_action :authenticate! + before_action :set_expand + + include ActionController::HttpAuthentication::Token::ControllerMethods + + rescue_from Pundit::NotAuthorizedError do |e| + render json: { error: "not_authorized" }, status: :forbidden + end + + rescue_from ActiveRecord::RecordNotFound do |e| + render json: { error: "resource_not_found", message: ("Couldn't locate that #{e.model.constantize.model_name.human}." if e.model) }.compact_blank, status: :not_found + end + + private + + def set_expand + @expand = params[:expand].to_s.split(",").map { |e| e.strip.to_sym } + end + + def authenticate! + @current_token = authenticate_with_http_token { |t, _options| Public::APIKey.find_by(token: t) } + unless @current_token&.active? + return render json: { error: "invalid_auth" }, status: :unauthorized + end + + @current_public_user = @current_token.public_user + end + end + end + end +end diff --git a/app/controllers/public/api/v1/letters_controller.rb b/app/controllers/public/api/v1/letters_controller.rb new file mode 100644 index 0000000..d4a8590 --- /dev/null +++ b/app/controllers/public/api/v1/letters_controller.rb @@ -0,0 +1,15 @@ +module Public + module API + module V1 + class LettersController < ApplicationController + def index + @letters = Letter.where(recipient_email: current_public_user.email) + end + + def show + @letter = Letter.where(recipient_email: current_public_user.email).find_by_public_id!(params[:id]) + end + end + end + end +end diff --git a/app/controllers/public/api/v1/lsv_controller.rb b/app/controllers/public/api/v1/lsv_controller.rb new file mode 100644 index 0000000..b6a675c --- /dev/null +++ b/app/controllers/public/api/v1/lsv_controller.rb @@ -0,0 +1,19 @@ +module Public + module API + module V1 + class LSVController < ApplicationController + def index + @lsv = LSV::TYPES.map { |type| type.find_by_email(current_public_user.email) }.flatten + render :index + end + + def show + @lsv = LSV::SLUGS[params[:slug].to_sym]&.find(params[:id]) + raise ActiveRecord::RecordNotFound unless @lsv && @lsv.email == current_public_user&.email + rescue Norairrecord::RecordNotFoundError + raise ActiveRecord::RecordNotFound + end + end + end + end +end diff --git a/app/controllers/public/api/v1/mail_controller.rb b/app/controllers/public/api/v1/mail_controller.rb new file mode 100644 index 0000000..5fcc1fd --- /dev/null +++ b/app/controllers/public/api/v1/mail_controller.rb @@ -0,0 +1,18 @@ +module Public + module API + module V1 + class MailController < ApplicationController + def index + @mail = + Warehouse::Order.where(recipient_email: current_public_user.email) + + Letter.where(recipient_email: current_public_user.email) + unless params[:no_load_lsv] + @mail += LSV::TYPES.map { |type| type.find_by_email(current_public_user.email) }.flatten + end + @mail.sort_by!(&:created_at).reverse! + render :index + end + end + end + end +end diff --git a/app/controllers/public/api/v1/packages_controller.rb b/app/controllers/public/api/v1/packages_controller.rb new file mode 100644 index 0000000..b46f0e5 --- /dev/null +++ b/app/controllers/public/api/v1/packages_controller.rb @@ -0,0 +1,22 @@ +module Public + module API + module V1 + class PackagesController < ApplicationController + before_action :set_package, only: [:show] + + def index + @packages = Warehouse::Order.where(recipient_email: current_public_user.email) + end + + def show + end + + private + + def set_package + @package = Warehouse::Order.find_by!(hc_id: params[:id]) + end + end + end + end +end diff --git a/app/controllers/public/api/v1/users_controller.rb b/app/controllers/public/api/v1/users_controller.rb new file mode 100644 index 0000000..c1e95da --- /dev/null +++ b/app/controllers/public/api/v1/users_controller.rb @@ -0,0 +1,12 @@ +module Public + module API + module V1 + class UsersController < ApplicationController + def me + @user = current_public_user + render :show + end + end + end + end +end diff --git a/app/controllers/public/api_keys_controller.rb b/app/controllers/public/api_keys_controller.rb new file mode 100644 index 0000000..bea3586 --- /dev/null +++ b/app/controllers/public/api_keys_controller.rb @@ -0,0 +1,43 @@ +module Public + class APIKeysController < ApplicationController + before_action :set_api_key, except: [:index, :new, :create] + before_action :authenticate_public_user! + + def index + @api_keys = APIKey.where(public_user: current_public_user) + end + + def new + @api_key = APIKey.new(public_user: current_public_user) + end + + def create + @api_key = APIKey.new(params.require(:public_api_key).permit(:name).merge(public_user: current_public_user)) + + if @api_key.save + redirect_to public_api_key_path(@api_key) + else + flash[:error] = @api_key.errors.full_messages.to_sentence + redirect_to new_public_api_key_path + end + end + + def show + end + + def revoke_confirm + end + + def revoke + @api_key.revoke! + flash[:success] = "terminated with extreme prejudice." + redirect_to public_api_key_path(@api_key) + end + + private + + def set_api_key + @api_key = APIKey.where(public_user: current_public_user).find(params[:id]) + end + end +end diff --git a/app/controllers/public/application_controller.rb b/app/controllers/public/application_controller.rb new file mode 100644 index 0000000..4f90848 --- /dev/null +++ b/app/controllers/public/application_controller.rb @@ -0,0 +1,52 @@ +module Public + class ApplicationController < ActionController::Base + include Pundit::Authorization + + layout "public" + + before_action do + Honeybadger.context({ + user_id: current_public_user&.id, + user_email: current_public_user&.email, + real_user_id: current_user&.id, + real_user_email: current_user&.email, + impersonator_user_id: session[:public_impersonator_user_id], + }) + end + + helper_method :current_user, :current_public_user, :public_user_signed_in?, :authenticate_public_user!, :impersonating? + + # DO NOT USE (in most cases :-P) + def current_user + @current_user ||= ::User.find_by(id: session[:user_id]) if session[:user_id] + end + + def current_public_user + @current_public_user ||= Public::User.find_by(id: session[:public_user_id]) if session[:public_user_id] + end + + def public_user_signed_in? + !!current_public_user + end + + def authenticate_public_user! + unless public_user_signed_in? + redirect_to public_login_path, alert: ("you need to be logged in!" unless request.env["PATH_INFO"] == "/") + end + end + + def impersonating? + !!session[:public_impersonator_user_id] + end + + rescue_from Pundit::NotAuthorizedError do |e| + flash[:error] = "hey, you can't do that!" + redirect_to public_root_path + end + + rescue_from ActiveRecord::RecordNotFound do |e| + flash[:error] = "sorry, couldn't find that page!" + redirect_to public_root_path + end + end +end diff --git a/app/controllers/public/impersonations_controller.rb b/app/controllers/public/impersonations_controller.rb new file mode 100644 index 0000000..679f1e0 --- /dev/null +++ b/app/controllers/public/impersonations_controller.rb @@ -0,0 +1,33 @@ +module Public + class ImpersonationsController < ApplicationController + def new + authorize Impersonation + @impersonation = Impersonation.new + end + + def create + @impersonation = Impersonation.new(impersonation_params.merge(user: current_user)) + + authorize @impersonation + + if @impersonation.save + public_user = Public::User.find_or_create_by!(email: impersonation_params[:target_email]) + session[:public_user_id] = public_user.id + session[:public_impersonator_user_id] = current_user.id + redirect_to public_root_path + else + render :new + end + end + + def stop_impersonating + session[:public_user_id] = nil + session[:public_impersonator_user_id] = nil + redirect_to public_root_path + end + + def impersonation_params + params.require(:public_impersonation).permit(:target_email, :justification) + end + end +end \ No newline at end of file diff --git a/app/controllers/public/leaderboards_controller.rb b/app/controllers/public/leaderboards_controller.rb new file mode 100644 index 0000000..ec475f0 --- /dev/null +++ b/app/controllers/public/leaderboards_controller.rb @@ -0,0 +1,49 @@ +module Public + class LeaderboardsController < ApplicationController + layout 'public/frameable' + + before_action :set_framed + + def this_week + @tab = :this_week + @lb = fetch_leaderboard(:week, Time.current.beginning_of_week) + render :show + end + + def this_month + @tab = :this_month + @lb = fetch_leaderboard(:month, Time.current.beginning_of_month) + render :show + end + + def all_time + @tab = :all_time + @lb = fetch_leaderboard(:all_time) + render :show + end + + private + + def set_framed + @framed = true + end + + def fetch_leaderboard(period, start_time = nil) + cache_key = "letter_leaderboard/#{period}" + cache_key += "/#{start_time.to_i}" if start_time + + Rails.cache.fetch(cache_key, expires_in: 10.minutes) do + query = ::User.joins(:letters) + .where(letters: { aasm_state: ['mailed', 'received'] }) + .group('users.id') + .select('users.*, COUNT(letters.id) as letter_count') + .having('COUNT(letters.id) > 0') + .order('letter_count DESC') + .limit(100) + + query = query.where('letters.mailed_at >= ?', start_time) if start_time + query + end + end + end +end \ No newline at end of file diff --git a/app/controllers/public/letters_controller.rb b/app/controllers/public/letters_controller.rb new file mode 100644 index 0000000..d882dae --- /dev/null +++ b/app/controllers/public/letters_controller.rb @@ -0,0 +1,41 @@ +module Public + class LettersController < ApplicationController + include Frameable + + before_action :set_letter + + def show + @framed = params[:framed].present? ? params[:framed] == 'true' : request.headers["Sec-Fetch-Dest"] == "iframe" + render "public/letters/show" + end + + def mark_received + @framed = params[:framed] + + if @letter.may_mark_received? + @letter.mark_received! + @received = true + frame_aware_redirect_to public_letter_path(@letter) + else + flash[:alert] = "huh?" + return frame_aware_redirect_to public_letter_path(@letter) + end + end + + def mark_mailed + if @letter.may_mark_mailed? + @letter.mark_mailed! + frame_aware_redirect_to public_letter_path(@letter) + else + flash[:alert] = "huh?" + return frame_aware_redirect_to public_letter_path(@letter) + end + end + + private + def set_letter + @letter = Letter.find_by_public_id!(params[:id]) + @events = @letter.events + end + end +end \ No newline at end of file diff --git a/app/controllers/public/lsv_controller.rb b/app/controllers/public/lsv_controller.rb new file mode 100644 index 0000000..09c1f26 --- /dev/null +++ b/app/controllers/public/lsv_controller.rb @@ -0,0 +1,31 @@ +module Public + class LSVController < ApplicationController + include Frameable + + def show + @lsv = LSV::SLUGS[params[:slug].to_sym]&.find(params[:id]) + raise ActiveRecord::RecordNotFound unless @lsv && @lsv.email == current_public_user&.email + rescue Norairrecord::RecordNotFoundError + raise ActiveRecord::RecordNotFound + end + + def customs_receipt + @msr = LSV::MarketingShipmentRequest.find(params[:id]) + raise ActiveRecord::RecordNotFound unless @msr && @msr.email == current_public_user&.email && @msr.country != "US" + rescue Norairrecord::RecordNotFoundError + raise ActiveRecord::RecordNotFound + end + + def generate_customs_receipt + @msr = LSV::MarketingShipmentRequest.find(params[:id]) + raise ActiveRecord::RecordNotFound unless @msr && @msr.email == current_public_user&.email && @msr.country != "US" + + CustomsReceipt::MSRReceiptJob.perform_later(@msr.id) + + flash[:success] = "check your email in a little bit!" + return redirect_to show_lsv_path(slug: "msr", id: @msr.id) + rescue Norairrecord::RecordNotFoundError + raise ActiveRecord::RecordNotFound + end + end +end diff --git a/app/controllers/public/mail_controller.rb b/app/controllers/public/mail_controller.rb new file mode 100644 index 0000000..c92b85d --- /dev/null +++ b/app/controllers/public/mail_controller.rb @@ -0,0 +1,15 @@ +module Public + class MailController < ApplicationController + before_action :authenticate_public_user! + + def index + @mail = + Warehouse::Order.where(recipient_email: current_public_user.email) + + Letter.where(recipient_email: current_public_user.email) + unless params[:no_load_lsv] + @mail += LSV::TYPES.map { |type| type.find_by_email(current_public_user.email) }.flatten + end + @mail.sort_by!(&:created_at).reverse! + end + end +end diff --git a/app/controllers/public/maps_controller.rb b/app/controllers/public/maps_controller.rb new file mode 100644 index 0000000..5d79289 --- /dev/null +++ b/app/controllers/public/maps_controller.rb @@ -0,0 +1,16 @@ +require "set" + +module Public + class MapsController < ApplicationController + include Frameable + layout "public/frameable" + + def show + @return_path = public_root_path + @letters_data = Rails.cache.fetch("map_data") do + # If cache is empty, run the job synchronously as a fallback + Public::UpdateMapDataJob.perform_now + end + end + end +end diff --git a/app/controllers/public/packages_controller.rb b/app/controllers/public/packages_controller.rb new file mode 100644 index 0000000..f173551 --- /dev/null +++ b/app/controllers/public/packages_controller.rb @@ -0,0 +1,36 @@ +module Public + class PackagesController < ApplicationController + include Frameable + before_action :set_package + + def show + if @package.is_a? Warehouse::Order + render "public/warehouse/orders/show" + end + end + + def customs_receipt + raise ActiveRecord::RecordNotFound unless @package.is_a?(Warehouse::Order) && + @package.recipient_email == current_public_user&.email && + !@package.address.us? + end + + def generate_customs_receipt + raise ActiveRecord::RecordNotFound unless @package.is_a?(Warehouse::Order) && + @package.recipient_email == current_public_user&.email && + !@package.address.us? + + # Queue the job to generate and send the customs receipt + CustomsReceipt::WarehouseOrderReceiptJob.perform_later(@package.id) + + flash[:success] = "check your email in a little bit!" + redirect_to public_package_path(@package) + end + + private + + def set_package + @package = Warehouse::Order.find_by!(hc_id: params[:id]) + end + end +end diff --git a/app/controllers/public/public_identifiable_controller.rb b/app/controllers/public/public_identifiable_controller.rb new file mode 100644 index 0000000..0dcae1c --- /dev/null +++ b/app/controllers/public/public_identifiable_controller.rb @@ -0,0 +1,21 @@ +module Public + class PublicIdentifiableController < ApplicationController + def show + prefix = params[:public_id].split("!").first&.downcase + + case prefix + when "ltr" + @record = Letter.find_by_public_id!(params[:public_id]) + redirect_to public_letter_path(@record) + when "pkg" + @record = Warehouse::Order.find_by_public_id!(params[:public_id]) + redirect_to public_package_path(@record) + else + raise ActiveRecord::RecordNotFound, "no record found with public_id: #{params[:public_id]}" + end + rescue ActiveRecord::RecordNotFound => e + flash[:alert] = "what are you even looking for..?" + redirect_to public_root_path + end + end +end \ No newline at end of file diff --git a/app/controllers/public/sessions_controller.rb b/app/controllers/public/sessions_controller.rb new file mode 100644 index 0000000..ca23f63 --- /dev/null +++ b/app/controllers/public/sessions_controller.rb @@ -0,0 +1,50 @@ +module Public + class SessionsController < ApplicationController + def send_email + begin + @email = params.require(:email) + rescue ActionController::ParameterMissing => e + @error = "you do need to enter an email address...." + return render "public/static_pages/login" + end + + if @email.ends_with?("hack.af") + @error = "come on, is that your real email? say hi to orpheus for me".html_safe + return render "public/static_pages/login" + end + + address = ValidEmail2::Address.new(@email) + + unless address.valid? + @error = "that isn't shaped like an email..." + return render "public/static_pages/login" + end + + if Rails.env.production? + SendLoginEmailJob.perform_later(@email) + else + SendLoginEmailJob.perform_now(@email) + end + end + + def login_code + valid_code = LoginCode.where(token: params[:token], used_at: nil) + .where("expires_at > ?", Time.current) + .first + + if valid_code + valid_code.mark_used! + session[:public_user_id] = valid_code.user_id + redirect_to public_root_path, notice: "you're in!" + else + redirect_to public_root_path, alert: "invalid or expired sign-in link...?" + end + end + + def destroy + session[:public_user_id] = nil + session[:public_impersonator_user_id] = nil + redirect_to public_root_path, notice: "signed out!" + end + end +end diff --git a/app/controllers/public/static_pages_controller.rb b/app/controllers/public/static_pages_controller.rb new file mode 100644 index 0000000..0edee9b --- /dev/null +++ b/app/controllers/public/static_pages_controller.rb @@ -0,0 +1,10 @@ +module Public + class StaticPagesController < ApplicationController + def root + # flash[:alert] = "bruh" + end + + def login + end + end +end diff --git a/app/controllers/public_ids_controller.rb b/app/controllers/public_ids_controller.rb new file mode 100644 index 0000000..8019dc7 --- /dev/null +++ b/app/controllers/public_ids_controller.rb @@ -0,0 +1,61 @@ +class PublicIdsController < ApplicationController + # GET /id/:public_id + # + skip_after_action :verify_authorized + # + def index + end + + def lookup + # The public_id contains the prefix that determines the model class + prefix = params[:id].split("!").first&.downcase + id_part = params[:id].split("!").last + + # Find the corresponding model based on the prefix + case prefix + when "mtr" + @record = USPS::IVMTR::Event.find_by_public_id!(params[:id]) + @letter = @record.letter + if current_user.admin? + redirect_to inspect_iv_mtr_event_path(@record) + else + if @letter.present? + redirect_to public_letter_path(@letter) + else + redirect_to public_ids_path, alert: "MTR event found, but no associated letter...?" + end + end + when "hackapost", "dev" + @indicium = USPS::Indicium.find(id_part[1...]) + @letter = @indicium.letter + if current_user.admin? + redirect_to inspect_indicium_path(@indicium) + else + if @letter.present? + redirect_to public_letter_path(@letter) + else + redirect_to public_ids_path, alert: "indicium found, but no associated letter...?" + end + end + else + # bad hack: + clazzes = ActiveRecord::Base.descendants.select { |c| c.included_modules.include?(PublicIdentifiable) } + clazz = clazzes.find { |c| c.public_id_prefix == prefix } + unless clazz.present? + return redirect_to public_ids_path, alert: "don't think we have that prefix: #{prefix}" + end + @record = clazz.find_by_public_id(params[:id]) + unless @record.present? + return redirect_to public_ids_path, alert: "no #{clazz.name} found with public id #{params[:id]}" + end + + redirect_to url_for(@record) + + # If no matching prefix is found, return 404 + # raise ActiveRecord::RecordNotFound, "No record found with public_id: #{params[:id]}" + end + rescue ActiveRecord::RecordNotFound => e + flash[:alert] = "Record not found" + redirect_to public_ids_path + end +end diff --git a/app/controllers/qz_trays_controller.rb b/app/controllers/qz_trays_controller.rb new file mode 100644 index 0000000..4827989 --- /dev/null +++ b/app/controllers/qz_trays_controller.rb @@ -0,0 +1,19 @@ +class QZTraysController < ApplicationController + skip_after_action :verify_authorized + skip_before_action :verify_authenticity_token, only: [:sign] + skip_before_action :authenticate_user!, only: [:test_print] + def cert + send_data QZTrayService.certificate + end + + def settings + end + + def sign + send_data QZTrayService.sign(params.require(:request)) + end + + def test_print + send_file(Rails.root.join('app', 'lib', 'test_print.pdf'), type: 'application/pdf', disposition: 'inline') + end +end \ No newline at end of file diff --git a/app/controllers/return_addresses_controller.rb b/app/controllers/return_addresses_controller.rb new file mode 100644 index 0000000..538b875 --- /dev/null +++ b/app/controllers/return_addresses_controller.rb @@ -0,0 +1,82 @@ +class ReturnAddressesController < ApplicationController + before_action :set_return_address, only: [:edit, :update, :destroy] + + def index + authorize ReturnAddress + @return_addresses = ReturnAddress.where(shared: true).or(ReturnAddress.where(user: current_user)) + end + + def new + authorize ReturnAddress + @return_address = ReturnAddress.new + @return_address.user = current_user if user_signed_in? + end + + def edit + authorize @return_address + end + + def create + @return_address = ReturnAddress.new(return_address_params) + @return_address.user = current_user if user_signed_in? + authorize @return_address + + if @return_address.save + # If this was created from the letter form, redirect back to the letter + if params[:from_letter].present? + redirect_to new_letter_path, notice: "Return address was successfully created. Please select it from the dropdown." + else + flash[:success] = "Return address was successfully created." + redirect_to return_addresses_path + end + else + render :new, status: :unprocessable_entity + end + end + + def update + authorize @return_address + + if @return_address.update(return_address_params) + # If this was updated from the letter form, redirect back to the letter + if params[:from_letter].present? + redirect_to new_letter_path, notice: "Return address was successfully updated. Please select it from the dropdown." + else + redirect_to @return_address, notice: "Return address was successfully updated." + end + else + render :edit, status: :unprocessable_entity + end + end + + def destroy + authorize @return_address + + if @return_address.letters.any? + redirect_to return_addresses_url, alert: "return address has letters associated with it, so it can't be deleted :-(" + else + @return_address.destroy + redirect_to return_addresses_url, notice: "Return address was successfully destroyed." + end + end + + def set_as_home + @return_address = ReturnAddress.find(params[:id]) + authorize @return_address + + current_user.update!(home_return_address: @return_address) + flash[:success] = "#{@return_address.display_name} is now your default return address." + + redirect_to return_addresses_url + end + + private + + def set_return_address + @return_address = ReturnAddress.find(params[:id]) + end + + def return_address_params + params.require(:return_address).permit(:name, :line_1, :line_2, :city, :state, :postal_code, :country, :shared, :user_id, :from_letter) + end +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 0000000..3211f56 --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,67 @@ +class SessionsController < ApplicationController + skip_before_action :authenticate_user!, only: [:new, :create] + + skip_after_action :verify_authorized + + def new + redirect_uri = url_for(action: :create, only_path: false) + Rails.logger.info "Starting Slack OAuth flow with redirect URI: #{redirect_uri}" + redirect_to User.authorize_url(redirect_uri), + host: "https://slack.com", + allow_other_host: true + end + + def create + redirect_uri = url_for(action: :create, only_path: false) + + if params[:error].present? + Rails.logger.error "Slack OAuth error: #{params[:error]}" + uuid = Honeybadger.notify("Slack OAuth error: #{params[:error]}") + redirect_to login_path, alert: "failed to authenticate with Slack! (error: #{uuid})" + return + end + + begin + @user = User.from_slack_token(params[:code], redirect_uri) + rescue => e + Rails.logger.error "Error creating user from Slack data: #{e.message}" + uuid = Honeybadger.notify(e) + redirect_to login_path, alert: "error authenticating! (error: #{uuid})" + return + end + + if @user&.persisted? + session[:user_id] = @user.id + flash[:success] = "welcome aboard!" + redirect_to root_path + else + Rails.logger.error "Failed to create/update user from Slack data" + redirect_to login_path, alert: "are you sure you should be here?" + end + end + + def impersonate + unless current_user.admin? + redirect_to root_path, alert: "you are not authorized to impersonate users. this incident has been reported :-P" + Honeybadger.notify("Impersonation attempt by #{current_user.username} to #{params[:id]}") + return + end + + session[:impersonator_user_id] ||= current_user.id + user = User.find(params[:id]) + session[:user_id] = user.id + flash[:success] = "hey #{user.username}! how's it going? nice 'stache and glasses!" + redirect_to root_path + end + + def stop_impersonating + session[:user_id] = session[:impersonator_user_id] + session[:impersonator_user_id] = nil + redirect_to root_path, notice: "welcome back, 007!" + end + + def destroy + session[:user_id] = nil + redirect_to root_path, notice: "bye, see you next time!" + end +end diff --git a/app/controllers/source_tags_controller.rb b/app/controllers/source_tags_controller.rb new file mode 100644 index 0000000..69573c8 --- /dev/null +++ b/app/controllers/source_tags_controller.rb @@ -0,0 +1,70 @@ +class SourceTagsController < ApplicationController + before_action :set_source_tag, only: %i[ show edit update destroy ] + + # GET /source_tags or /source_tags.json + def index + @source_tags = SourceTag.all + end + + # GET /source_tags/1 or /source_tags/1.json + def show + end + + # GET /source_tags/new + def new + @source_tag = SourceTag.new + end + + # GET /source_tags/1/edit + def edit + end + + # POST /source_tags or /source_tags.json + def create + @source_tag = SourceTag.new(source_tag_params) + + respond_to do |format| + if @source_tag.save + format.html { redirect_to @source_tag, notice: "Source tag was successfully created." } + format.json { render :show, status: :created, location: @source_tag } + else + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @source_tag.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /source_tags/1 or /source_tags/1.json + def update + respond_to do |format| + if @source_tag.update(source_tag_params) + format.html { redirect_to @source_tag, notice: "Source tag was successfully updated." } + format.json { render :show, status: :ok, location: @source_tag } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @source_tag.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /source_tags/1 or /source_tags/1.json + def destroy + @source_tag.destroy! + + respond_to do |format| + format.html { redirect_to source_tags_path, status: :see_other, notice: "Source tag was successfully destroyed." } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_source_tag + @source_tag = SourceTag.find(params.expect(:id)) + end + + # Only allow a list of trusted parameters through. + def source_tag_params + params.expect(source_tag: [ :slug, :name, :owner ]) + end +end diff --git a/app/controllers/static_pages_controller.rb b/app/controllers/static_pages_controller.rb new file mode 100644 index 0000000..92c6d78 --- /dev/null +++ b/app/controllers/static_pages_controller.rb @@ -0,0 +1,11 @@ +class StaticPagesController < ApplicationController + skip_before_action :authenticate_user!, only: [:login] + skip_after_action :verify_authorized + + def index + end + + def login + render :login, layout: false + end +end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb new file mode 100644 index 0000000..5fef592 --- /dev/null +++ b/app/controllers/tags_controller.rb @@ -0,0 +1,71 @@ +class TagsController < ApplicationController + skip_after_action :verify_authorized + # GET /tags or /tags.json + def index + @common_tags = CommonTag.all + + @tags = Rails.cache.fetch "tags_list" do + warehouse_order_tags = Warehouse::Order.all_tags + letter_tags = Letter.all_tags + + (warehouse_order_tags + letter_tags).uniq.compact_blank - @common_tags.map(&:tag) + end + end + + # GET /tags/1 or /tags/1.json + def show + tag = params[:id] + time_period = params[:time_period] || "all_time" + year = params[:year]&.to_i || Time.current.year + month = params[:month]&.to_i + + # Base queries + letter_query = Letter.with_any_tags([tag]).where.not(aasm_state: "queued") + wh_order_query = Warehouse::Order.with_any_tags([tag]) + + # Apply time period filter + case time_period + when "ytd" + start_date = Date.new(year, 1, 1) + letter_query = letter_query.where("created_at >= ?", start_date) + wh_order_query = wh_order_query.where("created_at >= ?", start_date) + when "month" + if month.present? + start_date = Date.new(year, month, 1) + end_date = start_date.end_of_month + letter_query = letter_query.where(created_at: start_date..end_date) + wh_order_query = wh_order_query.where(created_at: start_date..end_date) + end + when "last_week" + letter_query = letter_query.where("created_at >= ?", 1.week.ago) + wh_order_query = wh_order_query.where("created_at >= ?", 1.week.ago) + when "last_month" + letter_query = letter_query.where("created_at >= ?", 1.month.ago) + wh_order_query = wh_order_query.where("created_at >= ?", 1.month.ago) + when "last_year" + letter_query = letter_query.where("created_at >= ?", 1.year.ago) + wh_order_query = wh_order_query.where("created_at >= ?", 1.year.ago) + end + + @letter_count = letter_query.count + @letter_postage_cost = letter_query.sum(:postage) + @warehouse_order_count = wh_order_query.count + @warehouse_order_postage_cost = wh_order_query.sum(:postage_cost) + @warehouse_order_labor_cost = wh_order_query.sum(:labor_cost) + @warehouse_order_contents_cost = wh_order_query.sum(:contents_cost) + @warehouse_order_total_cost = @warehouse_order_postage_cost + @warehouse_order_labor_cost + @warehouse_order_contents_cost + + @tag = tag + @time_period = time_period + @year = year + @month = month + @years = (2020..Time.current.year).to_a.reverse + @months = (1..12).map { |m| [Date::MONTHNAMES[m], m] } + end + + def refresh + Rails.cache.delete("tags_list") + flash[:success] = "refreshed!" + redirect_to tags_path + end +end diff --git a/app/controllers/tasks_controller.rb b/app/controllers/tasks_controller.rb new file mode 100644 index 0000000..987a2f4 --- /dev/null +++ b/app/controllers/tasks_controller.rb @@ -0,0 +1,22 @@ +class TasksController < ApplicationController + skip_after_action :verify_authorized + before_action :find_tasks + + def badge + render :badge, layout: false + end + + def show + render :show + end + + def refresh + User::UpdateTasksJob.perform_now(current_user) + redirect_to tasks_path + end + + def find_tasks + @tasks = Rails.cache.read("user_tasks/#{current_user.id}") + @tasks ||= User::UpdateTasksJob.perform_now(current_user) + end +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb new file mode 100644 index 0000000..341554f --- /dev/null +++ b/app/controllers/users_controller.rb @@ -0,0 +1,70 @@ +class UsersController < ApplicationController + before_action :set_user, only: %i[ show edit update destroy ] + + # GET /users or /users.json + def index + @users = User.all + end + + # GET /users/1 or /users/1.json + def show + end + + # GET /users/new + def new + @user = User.new + end + + # GET /users/1/edit + def edit + end + + # POST /users or /users.json + def create + @user = User.new(user_params) + + respond_to do |format| + if @user.save + format.html { redirect_to @user, notice: "User was successfully created." } + format.json { render :show, status: :created, location: @user } + else + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @user.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /users/1 or /users/1.json + def update + respond_to do |format| + if @user.update(user_params) + format.html { redirect_to @user, notice: "User was successfully updated." } + format.json { render :show, status: :ok, location: @user } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @user.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /users/1 or /users/1.json + def destroy + @user.destroy! + + respond_to do |format| + format.html { redirect_to users_path, status: :see_other, notice: "User was successfully destroyed." } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_user + @user = User.find(params.expect(:id)) + end + + # Only allow a list of trusted parameters through. + def user_params + params.expect(user: [ :slack_id, :email, :is_admin ]) + end +end diff --git a/app/controllers/usps/iv_mtr/webhook_controller.rb b/app/controllers/usps/iv_mtr/webhook_controller.rb new file mode 100644 index 0000000..3cc46eb --- /dev/null +++ b/app/controllers/usps/iv_mtr/webhook_controller.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true +module USPS + module IVMTR + class WebhookController < ActionController::Base + + skip_before_action :verify_authenticity_token + + before_action do + http_basic_authenticate_or_request_with( + name: "my_best_friend_the_informed_visibility_robot", + password: Rails.application.credentials.dig(:usps, :iv_mtr, :webhook_password), + realm: "IV-MTR", + message: "nice try, jackwagon" + ) + end + + def ingest + data = JSON.parse(request.raw_post) + batch = USPS::IVMTR::RawJSONBatch.create( + payload: data["events"], + message_group_id: data["msgGrpId"], + processed: false + ) + USPS::IVMTR::ImportEventsJob.perform_later(batch) + render json: {message: "hey, thanks!"} + end + + end + + end +end diff --git a/app/controllers/warehouse/batches_controller.rb b/app/controllers/warehouse/batches_controller.rb new file mode 100644 index 0000000..dc1a066 --- /dev/null +++ b/app/controllers/warehouse/batches_controller.rb @@ -0,0 +1,137 @@ +class Warehouse::BatchesController < BaseBatchesController + before_action :set_allowed_templates, only: %i[ new create ] + + # GET /warehouse/batches or /warehouse/batches.json + def index + authorize Warehouse::Batch + @batches = policy_scope(Warehouse::Batch).order(created_at: :desc) + end + + # GET /warehouse/batches/1 or /warehouse/batches/1.json + def show + authorize @batch + end + + # GET /warehouse/batches/new + def new + authorize Warehouse::Batch + @batch = Warehouse::Batch.new + end + + # GET /warehouse/batches/1/edit + def edit + authorize @batch + end + + # POST /warehouse/batches or /warehouse/batches.json + def create + authorize Warehouse::Batch + @batch = Warehouse::Batch.new(batch_params.merge(user: current_user)) + + if @batch.save + @batch.aasm_state = :awaiting_field_mapping + @batch.save! + redirect_to map_fields_warehouse_batch_path(@batch), notice: "Please map your CSV fields to address fields." + else + render :new, status: :unprocessable_entity + end + end + + # PATCH/PUT /warehouse/batches/1 or /warehouse/batches/1.json + def update + authorize @batch + if @batch.update(batch_params) + # If template changed and batch hasn't been processed, recreate orders + if @batch.may_mark_processed? && @batch.saved_change_to_warehouse_template_id? + # Delete existing orders + @batch.orders.destroy_all + + # Recreate orders from addresses with new template + @batch.addresses.each do |address| + Warehouse::Order.from_template( + @batch.warehouse_template, + batch: @batch, + recipient_email: address.email, + address: address, + user: @batch.user, + idempotency_key: "batch_#{@batch.id}_address_#{address.id}", + user_facing_title: @batch.warehouse_user_facing_title, + tags: @batch.tags, + ).save! + end + end + + # Always update tags and user facing title on orders + @batch.orders.update_all( + tags: @batch.tags, + user_facing_title: @batch.warehouse_user_facing_title, + ) + + redirect_to warehouse_batch_path(@batch), notice: "Batch was successfully updated." + else + render :edit, status: :unprocessable_entity + end + end + + def destroy + authorize @batch + @batch.destroy + redirect_to warehouse_batches_path, notice: "Batch was successfully destroyed." + end + + def process_form + authorize @batch, :process_form? + render :process_warehouse + end + + def process_batch + authorize @batch, :process_batch? + if @batch.process! + redirect_to warehouse_batch_path(@batch), notice: "Batch was successfully processed." + else + render :process_form, status: :unprocessable_entity + end + end + + def set_mapping + authorize @batch, :set_mapping? + mapping = mapping_params.to_h + + # Invert the mapping to get from CSV columns to address fields + inverted_mapping = mapping.invert + + # Validate required fields + missing_fields = REQUIRED_FIELDS.reject { |field| inverted_mapping[field].present? } + + if missing_fields.any? + flash.now[:error] = "Please map the following required fields: #{missing_fields.join(", ")}" + render :map_fields, status: :unprocessable_entity + return + end + + if @batch.update!(field_mapping: inverted_mapping) + begin + @batch.run_map! + rescue StandardError => e + Rails.logger.warn(e) + uuid = Honeybadger.notify(e) + redirect_to warehouse_batch_path(@batch), flash: { alert: "Error mapping fields! #{e.message} (please report EID: #{uuid})" } + return + end + redirect_to process_confirm_warehouse_batch_path(@batch), notice: "Field mapping saved. Please review and process your batch." + else + flash.now[:error] = "Failed to save field mapping. #{@batch.errors.full_messages.join(", ")}" + render :map_fields, status: :unprocessable_entity + end + end + + private + + def batch_params + params.require(:batch).permit(:warehouse_template_id, :warehouse_user_facing_title, :csv, tags: []) + end + + def set_allowed_templates + @allowed_templates = Warehouse::Template.where(public: true).or(Warehouse::Template.where(user: current_user)) + end +end diff --git a/app/controllers/warehouse/orders_controller.rb b/app/controllers/warehouse/orders_controller.rb new file mode 100644 index 0000000..4bc3bfe --- /dev/null +++ b/app/controllers/warehouse/orders_controller.rb @@ -0,0 +1,137 @@ +class Warehouse::OrdersController < ApplicationController + before_action :set_warehouse_order, except: [:new, :create, :index] + # GET /warehouse/orders or /warehouse/orders.json + def index + authorize Warehouse::Order + + # Get all orders with their associations using policy scope + @all_orders = policy_scope(Warehouse::Order).includes(:batch, :address, :source_tag, :user) + + # Filter by batched/unbatched based on view parameter + if params[:view] == "batched" + @warehouse_orders = @all_orders.in_batch + else + @warehouse_orders = @all_orders.not_in_batch.page(params[:page]).per(20) + end + end + + # GET /warehouse/orders/1 or /warehouse/orders/1.json + def show + authorize @warehouse_order + end + + # GET /warehouse/orders/new + def new + authorize Warehouse::Order + @warehouse_order = Warehouse::Order.new + @warehouse_order.build_address + end + + # GET /warehouse/orders/1/edit + def edit + authorize @warehouse_order + end + + def send_to_warehouse + authorize @warehouse_order + + begin + @warehouse_order.dispatch! + rescue Zenventory::ZenventoryError => e + uuid = Honeybadger.notify(e) + redirect_to @warehouse_order, alert: "zenventory said \"#{e.message}\" (please report EID: #{uuid})" + rescue AASM::InvalidTransition => e + uuid = Honeybadger.notify(e) + redirect_to @warehouse_order, alert: "couldn't dispatch order! wrong state? (please report EID: #{uuid})" + end + redirect_to @warehouse_order, flash: { success: "successfully sent to warehouse!" } + end + + # POST /warehouse/orders or /warehouse/orders.json + def create + @warehouse_order = Warehouse::Order.new( + warehouse_order_params.merge( + user: current_user, + source_tag: SourceTag.web_tag, + ) + ) + + authorize @warehouse_order + + respond_to do |format| + if @warehouse_order.save + format.html { redirect_to @warehouse_order, notice: "Order was successfully created." } + format.json { render :show, status: :created, location: @warehouse_order } + else + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @warehouse_order.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /warehouse/orders/1 or /warehouse/orders/1.json + def update + authorize @warehouse_order + respond_to do |format| + if @warehouse_order.update(warehouse_order_params) + format.html { redirect_to @warehouse_order, notice: "Order was successfully updated." } + format.json { render :show, status: :ok, location: @warehouse_order } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @warehouse_order.errors, status: :unprocessable_entity } + end + end + end + + def cancel + authorize @warehouse_order + unless @warehouse_order.may_mark_canceled? + redirect_back_or_to @warehouse_order, alert: "order is not in a cancelable state!" + end + end + + def confirm_cancel + authorize @warehouse_order, :cancel? + + reason = params.require(:cancellation_reason) + begin + @warehouse_order.cancel!(reason) + rescue Zenventory::ZenventoryError => e + redirect_to @warehouse_order, alert: "couldn't cancel order! zenventory said: #{e.message}" + rescue AASM::InvalidTransition => e + redirect_to @warehouse_order, alert: "couldn't cancel order! wrong state?" + end + end + + # # DELETE /warehouse/orders/1 or /warehouse/orders/1.json + def destroy + authorize @warehouse_order + @warehouse_order.destroy! + + respond_to do |format| + format.html { redirect_to warehouse_orders_path, status: :see_other, notice: "it's gone." } + format.json { head :no_content } + end + end + + private + + # Use callbacks to share common setup or constraints between actions. + def set_warehouse_order + @warehouse_order = Warehouse::Order.find_by!(hc_id: params.expect(:id)) + end + + # Only allow a list of trusted parameters through. + def warehouse_order_params + params.require(:warehouse_order).permit( + :user_facing_title, + :user_facing_description, + :internal_notes, + :recipient_email, + :notify_on_dispatch, + tags: [], + line_items_attributes: [:id, :sku_id, :quantity, :_destroy], + address_attributes: %i[first_name last_name line_1 line_2 city state postal_code country phone_number email], + ).compact_blank + end +end diff --git a/app/controllers/warehouse/skus_controller.rb b/app/controllers/warehouse/skus_controller.rb new file mode 100644 index 0000000..140318b --- /dev/null +++ b/app/controllers/warehouse/skus_controller.rb @@ -0,0 +1,68 @@ +class Warehouse::SKUsController < ApplicationController + before_action :set_warehouse_sku, only: %i[ show edit update ] + + # GET /warehouse/skus or /warehouse/skus.json + def index + authorize Warehouse::SKU + @warehouse_skus = params[:include_non_inventory] ? Warehouse::SKU.all : Warehouse::SKU.in_inventory + end + + # GET /warehouse/skus/1 or /warehouse/skus/1.json + def show + authorize @warehouse_sku + end + + # GET /warehouse/skus/new + def new + authorize Warehouse::SKU + @warehouse_sku = Warehouse::SKU.new + end + + # GET /warehouse/skus/1/edit + def edit + authorize @warehouse_sku + end + + # POST /warehouse/skus or /warehouse/skus.json + def create + @warehouse_sku = Warehouse::SKU.new(warehouse_sku_params) + + authorize @warehouse_sku + + respond_to do |format| + if @warehouse_sku.save + format.html { redirect_to @warehouse_sku, notice: "WarehouseSKU was successfully created." } + format.json { render :show, status: :created, location: @warehouse_sku } + else + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @warehouse_sku.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /warehouse/skus/1 or /warehouse/skus/1.json + def update + authorize @warehouse_sku + respond_to do |format| + if @warehouse_sku.update(warehouse_sku_params) + format.html { redirect_to @warehouse_sku, notice: "WarehouseSKU was successfully updated." } + format.json { render :show, status: :ok, location: @warehouse_sku } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @warehouse_sku.errors, status: :unprocessable_entity } + end + end + end + + + private + # Use callbacks to share common setup or constraints between actions. + def set_warehouse_sku + @warehouse_sku = Warehouse::SKU.find(params.expect(:id)) + end + + # Only allow a list of trusted parameters through. + def warehouse_sku_params + params.expect(warehouse_sku: [ :sku, :description, :unit_cost, :customs_description, :in_stock, :ai_enabled, :enabled ]) + end +end diff --git a/app/controllers/warehouse/templates_controller.rb b/app/controllers/warehouse/templates_controller.rb new file mode 100644 index 0000000..f5bb368 --- /dev/null +++ b/app/controllers/warehouse/templates_controller.rb @@ -0,0 +1,80 @@ +class Warehouse::TemplatesController < ApplicationController + before_action :set_warehouse_template, only: %i[ show edit update destroy ] + + # GET /warehouse/templates or /warehouse/templates.json + def index + authorize Warehouse::Template + @warehouse_templates = Warehouse::Template.all + end + + # GET /warehouse/templates/1 or /warehouse/templates/1.json + def show + authorize @warehouse_template + end + + # GET /warehouse/templates/new + def new + authorize Warehouse::Template + @warehouse_template = Warehouse::Template.new + end + + # GET /warehouse/templates/1/edit + def edit + authorize @warehouse_template + end + + # POST /warehouse/templates or /warehouse/templates.json + def create + @warehouse_template = Warehouse::Template.new(warehouse_template_params.merge(user: current_user, source_tag: SourceTag.web_tag)) + authorize @warehouse_template + + respond_to do |format| + if @warehouse_template.save + format.html { redirect_to @warehouse_template, notice: "Template was successfully created." } + format.json { render :show, status: :created, location: @warehouse_template } + else + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @warehouse_template.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /warehouse/templates/1 or /warehouse/templates/1.json + def update + authorize @warehouse_template + respond_to do |format| + if @warehouse_template.update(warehouse_template_params) + format.html { redirect_to @warehouse_template, notice: "Template was successfully updated." } + format.json { render :show, status: :ok, location: @warehouse_template } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @warehouse_template.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /warehouse/templates/1 or /warehouse/templates/1.json + def destroy + authorize @warehouse_template + @warehouse_template.destroy! + + respond_to do |format| + format.html { redirect_to warehouse_templates_path, status: :see_other, notice: "Template was successfully destroyed." } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_warehouse_template + @warehouse_template = Warehouse::Template.find(params.require(:id)) + end + + # Only allow a list of trusted parameters through. + def warehouse_template_params + params.require(:warehouse_template).permit( + :name, :public, + line_items_attributes: [ :id, :sku_id, :quantity, :_destroy ] + ) + end +end diff --git a/app/dashboards/address_dashboard.rb b/app/dashboards/address_dashboard.rb new file mode 100644 index 0000000..7a296a8 --- /dev/null +++ b/app/dashboards/address_dashboard.rb @@ -0,0 +1,87 @@ +require "administrate/base_dashboard" + +class AddressDashboard < Administrate::BaseDashboard + # ATTRIBUTE_TYPES + # a hash that describes the type of each of the model's fields. + # + # Each different type represents an Administrate::Field object, + # which determines how the attribute is displayed + # on pages throughout the dashboard. + ATTRIBUTE_TYPES = { + id: Field::Number, + city: Field::String, + country: Field::Select.with_options(searchable: false, collection: ->(field) { field.resource.class.send(field.attribute.to_s.pluralize).keys }), + first_name: Field::String, + last_name: Field::String, + line_1: Field::String, + line_2: Field::String, + phone_number: Field::String, + postal_code: Field::String, + state: Field::String, + created_at: Field::DateTime, + updated_at: Field::DateTime + }.freeze + + # COLLECTION_ATTRIBUTES + # an array of attributes that will be displayed on the model's index page. + # + # By default, it's limited to four items to reduce clutter on index pages. + # Feel free to add, remove, or rearrange items. + COLLECTION_ATTRIBUTES = %i[ + id + city + country + first_name + ].freeze + + # SHOW_PAGE_ATTRIBUTES + # an array of attributes that will be displayed on the model's show page. + SHOW_PAGE_ATTRIBUTES = %i[ + id + city + country + first_name + last_name + line_1 + line_2 + phone_number + postal_code + state + created_at + updated_at + ].freeze + + # FORM_ATTRIBUTES + # an array of attributes that will be displayed + # on the model's form (`new` and `edit`) pages. + FORM_ATTRIBUTES = %i[ + city + country + first_name + last_name + line_1 + line_2 + phone_number + postal_code + state + ].freeze + + # COLLECTION_FILTERS + # a hash that defines filters that can be used while searching via the search + # field of the dashboard. + # + # For example to add an option to search for open resources by typing "open:" + # in the search field: + # + # COLLECTION_FILTERS = { + # open: ->(resources) { resources.where(open: true) } + # }.freeze + COLLECTION_FILTERS = {}.freeze + + # Overwrite this method to customize how addresses are displayed + # across all pages of the admin dashboard. + # + # def display_resource(address) + # "Address ##{address.id}" + # end +end diff --git a/app/dashboards/common_tag_dashboard.rb b/app/dashboards/common_tag_dashboard.rb new file mode 100644 index 0000000..5a03c0b --- /dev/null +++ b/app/dashboards/common_tag_dashboard.rb @@ -0,0 +1,66 @@ +require "administrate/base_dashboard" + +class CommonTagDashboard < Administrate::BaseDashboard + # ATTRIBUTE_TYPES + # a hash that describes the type of each of the model's fields. + # + # Each different type represents an Administrate::Field object, + # which determines how the attribute is displayed + # on pages throughout the dashboard. + ATTRIBUTE_TYPES = { + id: Field::Number, + implies_ysws: Field::Boolean, + tag: Field::String, + created_at: Field::DateTime, + updated_at: Field::DateTime, + }.freeze + + # COLLECTION_ATTRIBUTES + # an array of attributes that will be displayed on the model's index page. + # + # By default, it's limited to four items to reduce clutter on index pages. + # Feel free to add, remove, or rearrange items. + COLLECTION_ATTRIBUTES = %i[ + id + implies_ysws + tag + created_at + ].freeze + + # SHOW_PAGE_ATTRIBUTES + # an array of attributes that will be displayed on the model's show page. + SHOW_PAGE_ATTRIBUTES = %i[ + id + implies_ysws + tag + created_at + updated_at + ].freeze + + # FORM_ATTRIBUTES + # an array of attributes that will be displayed + # on the model's form (`new` and `edit`) pages. + FORM_ATTRIBUTES = %i[ + implies_ysws + tag + ].freeze + + # COLLECTION_FILTERS + # a hash that defines filters that can be used while searching via the search + # field of the dashboard. + # + # For example to add an option to search for open resources by typing "open:" + # in the search field: + # + # COLLECTION_FILTERS = { + # open: ->(resources) { resources.where(open: true) } + # }.freeze + COLLECTION_FILTERS = {}.freeze + + # Overwrite this method to customize how common tags are displayed + # across all pages of the admin dashboard. + # + # def display_resource(common_tag) + # "CommonTag ##{common_tag.id}" + # end +end diff --git a/app/dashboards/return_address_dashboard.rb b/app/dashboards/return_address_dashboard.rb new file mode 100644 index 0000000..b87a4f4 --- /dev/null +++ b/app/dashboards/return_address_dashboard.rb @@ -0,0 +1,90 @@ +require "administrate/base_dashboard" + +class ReturnAddressDashboard < Administrate::BaseDashboard + # ATTRIBUTE_TYPES + # a hash that describes the type of each of the model's fields. + # + # Each different type represents an Administrate::Field object, + # which determines how the attribute is displayed + # on pages throughout the dashboard. + ATTRIBUTE_TYPES = { + id: Field::Number, + name: Field::String, + line_1: Field::String, + line_2: Field::String, + city: Field::String, + country: Field::Select.with_options(searchable: false, collection: ->(field) { field.resource.class.send(field.attribute.to_s.pluralize).keys }), + postal_code: Field::String, + shared: Field::Boolean, + state: Field::String, + user: Field::BelongsTo.with_options(required: false), + created_at: Field::DateTime, + updated_at: Field::DateTime + }.freeze + + # COLLECTION_ATTRIBUTES + # an array of attributes that will be displayed on the model's index page. + # + # By default, it's limited to four items to reduce clutter on index pages. + # Feel free to add, remove, or rearrange items. + COLLECTION_ATTRIBUTES = %i[ + name + user + shared + city + ].freeze + + # SHOW_PAGE_ATTRIBUTES + # an array of attributes that will be displayed on the model's show page. + SHOW_PAGE_ATTRIBUTES = %i[ + id + name + line_1 + line_2 + city + state + postal_code + country + shared + user + created_at + updated_at + ].freeze + + # FORM_ATTRIBUTES + # an array of attributes that will be displayed + # on the model's form (`new` and `edit`) pages. + FORM_ATTRIBUTES = %i[ + name + line_1 + line_2 + city + state + postal_code + country + shared + user + ].freeze + + # COLLECTION_FILTERS + # a hash that defines filters that can be used while searching via the search + # field of the dashboard. + # + # For example to add an option to search for open resources by typing "open:" + # in the search field: + # + # COLLECTION_FILTERS = { + # open: ->(resources) { resources.where(open: true) } + # }.freeze + COLLECTION_FILTERS = { + shared: ->(resources) { resources.where(shared: true) }, + unshared: ->(resources) { resources.where(shared: false) } + }.freeze + + # Overwrite this method to customize how return addresses are displayed + # across all pages of the admin dashboard. + # + def display_resource(return_address) + return_address.name + end +end diff --git a/app/dashboards/source_tag_dashboard.rb b/app/dashboards/source_tag_dashboard.rb new file mode 100644 index 0000000..49e5335 --- /dev/null +++ b/app/dashboards/source_tag_dashboard.rb @@ -0,0 +1,72 @@ +require "administrate/base_dashboard" + +class SourceTagDashboard < Administrate::BaseDashboard + # ATTRIBUTE_TYPES + # a hash that describes the type of each of the model's fields. + # + # Each different type represents an Administrate::Field object, + # which determines how the attribute is displayed + # on pages throughout the dashboard. + ATTRIBUTE_TYPES = { + id: Field::Number, + name: Field::String, + owner: Field::String, + slug: Field::String, + warehouse_orders: Field::HasMany, + created_at: Field::DateTime, + updated_at: Field::DateTime + }.freeze + + # COLLECTION_ATTRIBUTES + # an array of attributes that will be displayed on the model's index page. + # + # By default, it's limited to four items to reduce clutter on index pages. + # Feel free to add, remove, or rearrange items. + COLLECTION_ATTRIBUTES = %i[ + id + name + owner + slug + ].freeze + + # SHOW_PAGE_ATTRIBUTES + # an array of attributes that will be displayed on the model's show page. + SHOW_PAGE_ATTRIBUTES = %i[ + id + name + owner + slug + warehouse_orders + created_at + updated_at + ].freeze + + # FORM_ATTRIBUTES + # an array of attributes that will be displayed + # on the model's form (`new` and `edit`) pages. + FORM_ATTRIBUTES = %i[ + name + owner + slug + warehouse_orders + ].freeze + + # COLLECTION_FILTERS + # a hash that defines filters that can be used while searching via the search + # field of the dashboard. + # + # For example to add an option to search for open resources by typing "open:" + # in the search field: + # + # COLLECTION_FILTERS = { + # open: ->(resources) { resources.where(open: true) } + # }.freeze + COLLECTION_FILTERS = {}.freeze + + # Overwrite this method to customize how source tags are displayed + # across all pages of the admin dashboard. + # + def display_resource(source_tag) + source_tag.slug + end +end diff --git a/app/dashboards/user_dashboard.rb b/app/dashboards/user_dashboard.rb new file mode 100644 index 0000000..90586d1 --- /dev/null +++ b/app/dashboards/user_dashboard.rb @@ -0,0 +1,87 @@ +require "administrate/base_dashboard" + +class UserDashboard < Administrate::BaseDashboard + # ATTRIBUTE_TYPES + # a hash that describes the type of each of the model's fields. + # + # Each different type represents an Administrate::Field object, + # which determines how the attribute is displayed + # on pages throughout the dashboard. + ATTRIBUTE_TYPES = { + id: Field::Number, + can_warehouse: Field::Boolean, + email: Field::String, + icon_url: Field::String, + is_admin: Field::Boolean, + slack_id: Field::String, + username: Field::String, + warehouse_templates: Field::HasMany, + home_mid: Field::BelongsTo, + home_return_address: Field::BelongsTo, + created_at: Field::DateTime, + updated_at: Field::DateTime, + }.freeze + + # COLLECTION_ATTRIBUTES + # an array of attributes that will be displayed on the model's index page. + # + # By default, it's limited to four items to reduce clutter on index pages. + # Feel free to add, remove, or rearrange items. + COLLECTION_ATTRIBUTES = %i[ + username + is_admin + can_warehouse + email + slack_id + ].freeze + + # SHOW_PAGE_ATTRIBUTES + # an array of attributes that will be displayed on the model's show page. + SHOW_PAGE_ATTRIBUTES = %i[ + id + can_warehouse + email + icon_url + is_admin + slack_id + username + warehouse_templates + home_mid + home_return_address + created_at + updated_at + ].freeze + + # FORM_ATTRIBUTES + # an array of attributes that will be displayed + # on the model's form (`new` and `edit`) pages. + FORM_ATTRIBUTES = %i[ + can_warehouse + email + icon_url + is_admin + slack_id + username + home_mid + home_return_address + ].freeze + + # COLLECTION_FILTERS + # a hash that defines filters that can be used while searching via the search + # field of the dashboard. + # + # For example to add an option to search for open resources by typing "open:" + # in the search field: + # + # COLLECTION_FILTERS = { + # open: ->(resources) { resources.where(open: true) } + # }.freeze + COLLECTION_FILTERS = {}.freeze + + # Overwrite this method to customize how users are displayed + # across all pages of the admin dashboard. + # + def display_resource(user) + user.username + end +end diff --git a/app/dashboards/usps/mailer_id_dashboard.rb b/app/dashboards/usps/mailer_id_dashboard.rb new file mode 100644 index 0000000..26aa1a7 --- /dev/null +++ b/app/dashboards/usps/mailer_id_dashboard.rb @@ -0,0 +1,71 @@ +require "administrate/base_dashboard" + +module USPS + class MailerIdDashboard < Administrate::BaseDashboard + # ATTRIBUTE_TYPES + # a hash that describes the type of each of the model's fields. + # + # Each different type represents an Administrate::Field object, + # which determines how the attribute is displayed + # on pages throughout the dashboard. + ATTRIBUTE_TYPES = { + id: Field::Number, + crid: Field::String, + mid: Field::String, + name: Field::String, + created_at: Field::DateTime, + updated_at: Field::DateTime + }.freeze + + # COLLECTION_ATTRIBUTES + # an array of attributes that will be displayed on the model's index page. + # + # By default, it's limited to four items to reduce clutter on index pages. + # Feel free to add, remove, or rearrange items. + COLLECTION_ATTRIBUTES = %i[ + id + crid + mid + name + ].freeze + + # SHOW_PAGE_ATTRIBUTES + # an array of attributes that will be displayed on the model's show page. + SHOW_PAGE_ATTRIBUTES = %i[ + id + crid + mid + name + created_at + updated_at + ].freeze + + # FORM_ATTRIBUTES + # an array of attributes that will be displayed + # on the model's form (`new` and `edit`) pages. + FORM_ATTRIBUTES = %i[ + crid + mid + name + ].freeze + + # COLLECTION_FILTERS + # a hash that defines filters that can be used while searching via the search + # field of the dashboard. + # + # For example to add an option to search for open resources by typing "open:" + # in the search field: + # + # COLLECTION_FILTERS = { + # open: ->(resources) { resources.where(open: true) } + # }.freeze + COLLECTION_FILTERS = {}.freeze + + # Overwrite this method to customize how mailer ids are displayed + # across all pages of the admin dashboard. + # + # def display_resource(mailer_id) + # "USPS::MailerId ##{mailer_id.id}" + # end + end +end diff --git a/app/dashboards/usps/payment_account_dashboard.rb b/app/dashboards/usps/payment_account_dashboard.rb new file mode 100644 index 0000000..55cc0a2 --- /dev/null +++ b/app/dashboards/usps/payment_account_dashboard.rb @@ -0,0 +1,86 @@ +require "administrate/base_dashboard" + +module USPS + class PaymentAccountDashboard < Administrate::BaseDashboard + # ATTRIBUTE_TYPES + # a hash that describes the type of each of the model's fields. + # + # Each different type represents an Administrate::Field object, + # which determines how the attribute is displayed + # on pages throughout the dashboard. + ATTRIBUTE_TYPES = { + id: Field::Number, + account_number: Field::String, + account_type: Field::Select.with_options(searchable: false, collection: ->(field) { field.resource.class.send(field.attribute.to_s.pluralize).keys }), + manifest_mid: Field::String, + name: Field::String, + permit_number: Field::String, + permit_zip: Field::String, + usps_mailer_id: Field::BelongsTo, + created_at: Field::DateTime, + updated_at: Field::DateTime, + ach: Field::Boolean, + }.freeze + + # COLLECTION_ATTRIBUTES + # an array of attributes that will be displayed on the model's index page. + # + # By default, it's limited to four items to reduce clutter on index pages. + # Feel free to add, remove, or rearrange items. + COLLECTION_ATTRIBUTES = %i[ + id + account_number + account_type + manifest_mid + ].freeze + + # SHOW_PAGE_ATTRIBUTES + # an array of attributes that will be displayed on the model's show page. + SHOW_PAGE_ATTRIBUTES = %i[ + id + account_number + account_type + manifest_mid + name + permit_number + permit_zip + usps_mailer_id + created_at + updated_at + ach + ].freeze + + # FORM_ATTRIBUTES + # an array of attributes that will be displayed + # on the model's form (`new` and `edit`) pages. + FORM_ATTRIBUTES = %i[ + account_number + account_type + manifest_mid + name + permit_number + permit_zip + usps_mailer_id + ach + ].freeze + + # COLLECTION_FILTERS + # a hash that defines filters that can be used while searching via the search + # field of the dashboard. + # + # For example to add an option to search for open resources by typing "open:" + # in the search field: + # + # COLLECTION_FILTERS = { + # open: ->(resources) { resources.where(open: true) } + # }.freeze + COLLECTION_FILTERS = {}.freeze + + # Overwrite this method to customize how payment accounts are displayed + # across all pages of the admin dashboard. + # + # def display_resource(payment_account) + # "USPS::PaymentAccount ##{payment_account.id}" + # end + end +end diff --git a/app/dashboards/warehouse/line_item_dashboard.rb b/app/dashboards/warehouse/line_item_dashboard.rb new file mode 100644 index 0000000..337818b --- /dev/null +++ b/app/dashboards/warehouse/line_item_dashboard.rb @@ -0,0 +1,74 @@ +require "administrate/base_dashboard" + +module Warehouse + class LineItemDashboard < Administrate::BaseDashboard + # ATTRIBUTE_TYPES + # a hash that describes the type of each of the model's fields. + # + # Each different type represents an Administrate::Field object, + # which determines how the attribute is displayed + # on pages throughout the dashboard. + ATTRIBUTE_TYPES = { + id: Field::Number, + order: Field::BelongsTo, + quantity: Field::Number, + sku: Field::BelongsTo, + template: Field::BelongsTo, + created_at: Field::DateTime, + updated_at: Field::DateTime + }.freeze + + # COLLECTION_ATTRIBUTES + # an array of attributes that will be displayed on the model's index page. + # + # By default, it's limited to four items to reduce clutter on index pages. + # Feel free to add, remove, or rearrange items. + COLLECTION_ATTRIBUTES = %i[ + id + order + quantity + sku + ].freeze + + # SHOW_PAGE_ATTRIBUTES + # an array of attributes that will be displayed on the model's show page. + SHOW_PAGE_ATTRIBUTES = %i[ + id + order + quantity + sku + template + created_at + updated_at + ].freeze + + # FORM_ATTRIBUTES + # an array of attributes that will be displayed + # on the model's form (`new` and `edit`) pages. + FORM_ATTRIBUTES = %i[ + order + quantity + sku + template + ].freeze + + # COLLECTION_FILTERS + # a hash that defines filters that can be used while searching via the search + # field of the dashboard. + # + # For example to add an option to search for open resources by typing "open:" + # in the search field: + # + # COLLECTION_FILTERS = { + # open: ->(resources) { resources.where(open: true) } + # }.freeze + COLLECTION_FILTERS = {}.freeze + + # Overwrite this method to customize how line items are displayed + # across all pages of the admin dashboard. + # + # def display_resource(line_item) + # "Warehouse::LineItem ##{line_item.id}" + # end + end +end diff --git a/app/dashboards/warehouse/order_dashboard.rb b/app/dashboards/warehouse/order_dashboard.rb new file mode 100644 index 0000000..4bea854 --- /dev/null +++ b/app/dashboards/warehouse/order_dashboard.rb @@ -0,0 +1,128 @@ +require "administrate/base_dashboard" + +module Warehouse + class OrderDashboard < Administrate::BaseDashboard + # ATTRIBUTE_TYPES + # a hash that describes the type of each of the model's fields. + # + # Each different type represents an Administrate::Field object, + # which determines how the attribute is displayed + # on pages throughout the dashboard. + ATTRIBUTE_TYPES = { + id: Field::Number, + aasm_state: Field::String, + address: Field::BelongsTo, + canceled_at: Field::DateTime, + carrier: Field::String, + dispatched_at: Field::DateTime, + hc_id: Field::String, + idempotency_key: Field::String, + internal_notes: Field::Text, + line_items: Field::HasMany, + mailed_at: Field::DateTime, + postage_cost: Field::String.with_options(searchable: false), + recipient_email: Field::String, + service: Field::String, + skus: Field::HasMany, + source_tag: Field::BelongsTo, + surprise: Field::Boolean, + tracking_number: Field::String, + user: Field::BelongsTo, + user_facing_description: Field::String, + user_facing_title: Field::String, + weight: Field::String.with_options(searchable: false), + zenventory_id: Field::Number, + created_at: Field::DateTime, + updated_at: Field::DateTime, + }.freeze + + # COLLECTION_ATTRIBUTES + # an array of attributes that will be displayed on the model's index page. + # + # By default, it's limited to four items to reduce clutter on index pages. + # Feel free to add, remove, or rearrange items. + COLLECTION_ATTRIBUTES = %i[ + hc_id + aasm_state + user + source_tag + ].freeze + + # SHOW_PAGE_ATTRIBUTES + # an array of attributes that will be displayed on the model's show page. + SHOW_PAGE_ATTRIBUTES = %i[ + id + aasm_state + address + canceled_at + carrier + dispatched_at + hc_id + idempotency_key + internal_notes + line_items + mailed_at + postage_cost + recipient_email + service + skus + source_tag + surprise + tracking_number + user + user_facing_description + user_facing_title + weight + zenventory_id + created_at + updated_at + ].freeze + + # FORM_ATTRIBUTES + # an array of attributes that will be displayed + # on the model's form (`new` and `edit`) pages. + FORM_ATTRIBUTES = %i[ + aasm_state + address + canceled_at + carrier + dispatched_at + hc_id + idempotency_key + internal_notes + line_items + mailed_at + postage_cost + recipient_email + service + skus + source_tag + surprise + tracking_number + user + user_facing_description + user_facing_title + weight + zenventory_id + ].freeze + + # COLLECTION_FILTERS + # a hash that defines filters that can be used while searching via the search + # field of the dashboard. + # + # For example to add an option to search for open resources by typing "open:" + # in the search field: + # + # COLLECTION_FILTERS = { + # open: ->(resources) { resources.where(open: true) } + # }.freeze + COLLECTION_FILTERS = {}.freeze + + # Overwrite this method to customize how orders are displayed + # across all pages of the admin dashboard. + # + def display_resource(order) + "Order ##{order.hc_id}" + end + end +end diff --git a/app/dashboards/warehouse/sku_dashboard.rb b/app/dashboards/warehouse/sku_dashboard.rb new file mode 100644 index 0000000..74e91e4 --- /dev/null +++ b/app/dashboards/warehouse/sku_dashboard.rb @@ -0,0 +1,108 @@ +require "administrate/base_dashboard" + +module Warehouse + class SKUDashboard < Administrate::BaseDashboard + # ATTRIBUTE_TYPES + # a hash that describes the type of each of the model's fields. + # + # Each different type represents an Administrate::Field object, + # which determines how the attribute is displayed + # on pages throughout the dashboard. + ATTRIBUTE_TYPES = { + id: Field::Number, + actual_cost_to_hc: Field::String.with_options(searchable: false), + ai_enabled: Field::Boolean, + average_po_cost: Field::String.with_options(searchable: false), + category: Field::Select.with_options(searchable: false, collection: ->(field) { field.resource.class.send(field.attribute.to_s.pluralize).keys }), + country_of_origin: Field::String, + customs_description: Field::Text, + declared_unit_cost_override: Field::String.with_options(searchable: false), + description: Field::Text, + enabled: Field::Boolean, + hs_code: Field::String, + in_stock: Field::Number, + inbound: Field::Number, + name: Field::String, + sku: Field::String, + zenventory_id: Field::String, + created_at: Field::DateTime, + updated_at: Field::DateTime, + }.freeze + + # COLLECTION_ATTRIBUTES + # an array of attributes that will be displayed on the model's index page. + # + # By default, it's limited to four items to reduce clutter on index pages. + # Feel free to add, remove, or rearrange items. + COLLECTION_ATTRIBUTES = %i[ + sku + name + description + enabled + average_po_cost + ].freeze + + # SHOW_PAGE_ATTRIBUTES + # an array of attributes that will be displayed on the model's show page. + SHOW_PAGE_ATTRIBUTES = %i[ + id + actual_cost_to_hc + ai_enabled + average_po_cost + category + country_of_origin + customs_description + declared_unit_cost_override + description + enabled + hs_code + in_stock + inbound + name + sku + zenventory_id + created_at + updated_at + ].freeze + + # FORM_ATTRIBUTES + # an array of attributes that will be displayed + # on the model's form (`new` and `edit`) pages. + FORM_ATTRIBUTES = %i[ + actual_cost_to_hc + ai_enabled + average_po_cost + category + country_of_origin + customs_description + declared_unit_cost_override + description + enabled + hs_code + in_stock + inbound + name + sku + zenventory_id + ].freeze + + # COLLECTION_FILTERS + # a hash that defines filters that can be used while searching via the search + # field of the dashboard. + # + # For example to add an option to search for open resources by typing "open:" + # in the search field: + # + # COLLECTION_FILTERS = { + # open: ->(resources) { resources.where(open: true) } + # }.freeze + COLLECTION_FILTERS = {}.freeze + + # Overwrite this method to customize how skus are displayed + # across all pages of the admin dashboard. + # + def display_resource(sku) + "SKU #{sku.sku}" + end + end +end diff --git a/app/dashboards/warehouse/template_dashboard.rb b/app/dashboards/warehouse/template_dashboard.rb new file mode 100644 index 0000000..e776c1c --- /dev/null +++ b/app/dashboards/warehouse/template_dashboard.rb @@ -0,0 +1,80 @@ +require "administrate/base_dashboard" + +module Warehouse + class TemplateDashboard < Administrate::BaseDashboard + # ATTRIBUTE_TYPES + # a hash that describes the type of each of the model's fields. + # + # Each different type represents an Administrate::Field object, + # which determines how the attribute is displayed + # on pages throughout the dashboard. + ATTRIBUTE_TYPES = { + id: Field::Number, + line_items: Field::HasMany, + name: Field::String, + public: Field::Boolean, + skus: Field::HasMany, + source_tag: Field::BelongsTo, + user: Field::BelongsTo, + created_at: Field::DateTime, + updated_at: Field::DateTime + }.freeze + + # COLLECTION_ATTRIBUTES + # an array of attributes that will be displayed on the model's index page. + # + # By default, it's limited to four items to reduce clutter on index pages. + # Feel free to add, remove, or rearrange items. + COLLECTION_ATTRIBUTES = %i[ + id + line_items + name + public + ].freeze + + # SHOW_PAGE_ATTRIBUTES + # an array of attributes that will be displayed on the model's show page. + SHOW_PAGE_ATTRIBUTES = %i[ + id + line_items + name + public + skus + source_tag + user + created_at + updated_at + ].freeze + + # FORM_ATTRIBUTES + # an array of attributes that will be displayed + # on the model's form (`new` and `edit`) pages. + FORM_ATTRIBUTES = %i[ + line_items + name + public + skus + source_tag + user + ].freeze + + # COLLECTION_FILTERS + # a hash that defines filters that can be used while searching via the search + # field of the dashboard. + # + # For example to add an option to search for open resources by typing "open:" + # in the search field: + # + # COLLECTION_FILTERS = { + # open: ->(resources) { resources.where(open: true) } + # }.freeze + COLLECTION_FILTERS = {}.freeze + + # Overwrite this method to customize how templates are displayed + # across all pages of the admin dashboard. + # + # def display_resource(template) + # "Warehouse::Template ##{template.id}" + # end + end +end diff --git a/app/fields/administrate/field/array.rb b/app/fields/administrate/field/array.rb new file mode 100644 index 0000000..0a8cab7 --- /dev/null +++ b/app/fields/administrate/field/array.rb @@ -0,0 +1,31 @@ +require "administrate/field/base" + +class Administrate::Field::Array < Administrate::Field::Base + def to_s + data + end + + def self.permitted_attribute(attr, _options = nil) + { attr => [] } + end + + def self.searchable? + true + end + + def searchable? + self.class.searchable? + end + + def collection + data || [] + end + + def include_blank + options.fetch(:include_blank, true) + end + + def value + data + end +end \ No newline at end of file diff --git a/app/frontend/entrypoints/98.css b/app/frontend/entrypoints/98.css new file mode 100644 index 0000000..aa31c0d --- /dev/null +++ b/app/frontend/entrypoints/98.css @@ -0,0 +1 @@ +@import "98.css/dist/98.css"; \ No newline at end of file diff --git a/app/frontend/entrypoints/app_style.scss b/app/frontend/entrypoints/app_style.scss new file mode 100644 index 0000000..6ac0627 --- /dev/null +++ b/app/frontend/entrypoints/app_style.scss @@ -0,0 +1,5 @@ +@use "../styles/colors" as *; +@use "../styles/app" as *; + +@import "selectize/dist/css/selectize"; +// @import "selectize/dist/css/selectize.default"; \ No newline at end of file diff --git a/app/frontend/entrypoints/application.js b/app/frontend/entrypoints/application.js new file mode 100644 index 0000000..31e2ff4 --- /dev/null +++ b/app/frontend/entrypoints/application.js @@ -0,0 +1 @@ +import '~/js/click-to-copy.js' \ No newline at end of file diff --git a/app/frontend/entrypoints/cocoon.js b/app/frontend/entrypoints/cocoon.js new file mode 100644 index 0000000..d302355 --- /dev/null +++ b/app/frontend/entrypoints/cocoon.js @@ -0,0 +1 @@ +import "@oddcamp/cocoon-vanilla-js"; \ No newline at end of file diff --git a/app/frontend/entrypoints/instant_print.js b/app/frontend/entrypoints/instant_print.js new file mode 100644 index 0000000..02bbabf --- /dev/null +++ b/app/frontend/entrypoints/instant_print.js @@ -0,0 +1,9 @@ +import 'dreamland' +let root = document.getElementById('instant_print_root') +import {InstantPrintWindow} from '~/js/components/instant_print_window' +import {connect_qz} from "./qz"; +root.appendChild(h(InstantPrintWindow, {pdf_url: root.dataset.url})) +await connect_qz(); +if(root.dataset.printNow) { + setTimeout(()=>{document.querySelector('[data-component="PrintButton"]>button').click()}) +} diff --git a/app/frontend/entrypoints/login_page.scss b/app/frontend/entrypoints/login_page.scss new file mode 100644 index 0000000..ee88617 --- /dev/null +++ b/app/frontend/entrypoints/login_page.scss @@ -0,0 +1 @@ +@use "@/styles/login.scss"; \ No newline at end of file diff --git a/app/frontend/entrypoints/map.js b/app/frontend/entrypoints/map.js new file mode 100644 index 0000000..2e6d257 --- /dev/null +++ b/app/frontend/entrypoints/map.js @@ -0,0 +1,4 @@ +import * as d3 from "d3"; +import * as topojson from "topojson"; +window.d3 = d3; +window.topojson = topojson; \ No newline at end of file diff --git a/app/frontend/entrypoints/public.js b/app/frontend/entrypoints/public.js new file mode 100644 index 0000000..fb4cd7c --- /dev/null +++ b/app/frontend/entrypoints/public.js @@ -0,0 +1,10 @@ +import $ from "jquery"; +window.$ = window.jQuery = $; +import '~/js/click-to-copy.js' +import '~/js/interactive-tables.js' +import '~/js/confetti.js' +import '~/js/draggable-windows.js' +import '~/js/turbo-confirm.js' +import '@hotwired/turbo-rails' + +Turbo.config.forms.mode = "optin"; \ No newline at end of file diff --git a/app/frontend/entrypoints/public.scss b/app/frontend/entrypoints/public.scss new file mode 100644 index 0000000..8e2072f --- /dev/null +++ b/app/frontend/entrypoints/public.scss @@ -0,0 +1 @@ +@use "@/styles/public.scss" as *; \ No newline at end of file diff --git a/app/frontend/entrypoints/qz.js b/app/frontend/entrypoints/qz.js new file mode 100644 index 0000000..556cbc4 --- /dev/null +++ b/app/frontend/entrypoints/qz.js @@ -0,0 +1,72 @@ +import qz from 'qz-tray' +import 'dreamland' + +let qzState = $state({ + status: "connecting", availablePrinters: [] +}) + +window.qz_state = qzState + +let qzSettingsStore = $store({ + printer: undefined, dpi: 203, +}, {ident: "theseus-qz-settings", backing: "localstorage", autosave: "auto"}) + +window.qz_settings = qzSettingsStore + +window.qz_disconnected = use(qz_state.status, s => (s !== "connected")) + +qz.security.setCertificatePromise(function (resolve, reject) { + fetch("/qz_tray/cert", {cache: 'no-store', headers: {'Content-Type': 'text/plain'}}) + .then(function (data) { + data.ok ? resolve(data.text()) : reject(data.text()); + }); +}); + +qz.security.setSignatureAlgorithm("SHA512"); // Since 2.1 +qz.security.setSignaturePromise(function (toSign) { + return function (resolve, reject) { + fetch("/qz_tray/sign?request=" + toSign, { + method: 'POST', cache: 'no-store', headers: {'Content-Type': 'text/plain'} + }) + .then(function (data) { + data.ok ? resolve(data.text()) : reject(data.text()); + }); + }; +}); + +async function connect_qz() { + await qz.websocket.connect().then(function () { + qzState.status = "connected" + }).catch(function (error) { + qzState.status = "error" + }) +} + +function make_qz_config() { + return qz.configs.create(qzSettingsStore.printer, { + colorType: 'blackwhite', + density: +qzSettingsStore.dpi, + units: 'in', + rasterize: true, + interpolation: 'nearest-neighbor', + size: { + width: 4, height: 6 + } + }) +} + +function print(input, flavor = 'file', onSuccess = () => {}) { + var data = [{ + type: 'pixel', format: 'pdf', flavor: 'file', data: input + }]; + qz.print(make_qz_config(), data) + .then(function() { + qzState.error = null; + if (onSuccess) onSuccess(); + }) + .catch(function (e) { + qzState.error = e; + }); +} + +export {qz, connect_qz, qzState, qzSettingsStore, print} diff --git a/app/frontend/entrypoints/qz_settings.js b/app/frontend/entrypoints/qz_settings.js new file mode 100644 index 0000000..b29d545 --- /dev/null +++ b/app/frontend/entrypoints/qz_settings.js @@ -0,0 +1,46 @@ +import {connect_qz, qz, qzState, qzSettingsStore, print} from './qz' +import 'dreamland' + +import {QZStatusBanner} from "../js/components/qz/status_banner"; +import {QZPrinterPicker} from "../js/components/qz/printer_picker"; +import {DPIPicker} from "../js/components/qz/dpi_picker"; +import {TestPrintButton} from "../js/components/qz/test_print_button"; +import {RefreshPrintersButton} from "~/js/components/qz/refresh_printers_button"; +let root = document.getElementById('dl_root') + + +function findPrinters() { + qz.printers.find().then(function (data) { + qz_state.refresh_state=true; + setTimeout(()=>(qz_state.refresh_state=false), 200); + qzState.availablePrinters = data + }).catch(function (e) { + console.error(e); + }); +} +qz_state.refresh_state = false +root.appendChild(h(QZStatusBanner, {in_settings: true})) +// root.appendChild(h(QZPrinterPicker)) +root.appendChild(html` +
+
+
+ <${QZPrinterPicker}/> +
+
+ ${h(RefreshPrintersButton, {findPrinters: findPrinters})} +
+
+
+ +`) + +// root.appendChild(h(RefreshPrintersButton, {findPrinters: findPrinters})) +root.appendChild(h(DPIPicker, {settings: qzSettingsStore, state: qzState})) +root.appendChild(h(TestPrintButton, { + testPrint: () => { + print("/qz_tray/test_print") + } +})) +await connect_qz(); +await findPrinters(); diff --git a/app/frontend/entrypoints/taggable.js b/app/frontend/entrypoints/taggable.js new file mode 100644 index 0000000..ca17852 --- /dev/null +++ b/app/frontend/entrypoints/taggable.js @@ -0,0 +1,13 @@ +import $ from "jquery"; +import "selectize"; + +$(".selectize-tags").selectize({ + delimiter: ",", + persist: false, + create: function (input) { + return { + value: input, + text: input, + }; + }, + }); \ No newline at end of file diff --git a/app/frontend/images/.keep b/app/frontend/images/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/frontend/images/icons/break-the-glass.png b/app/frontend/images/icons/break-the-glass.png new file mode 100644 index 0000000..4f0219e Binary files /dev/null and b/app/frontend/images/icons/break-the-glass.png differ diff --git a/app/frontend/images/icons/impersonate.png b/app/frontend/images/icons/impersonate.png new file mode 100644 index 0000000..801cabf Binary files /dev/null and b/app/frontend/images/icons/impersonate.png differ diff --git a/app/frontend/images/login/treasure.png b/app/frontend/images/login/treasure.png new file mode 100644 index 0000000..b7fbc47 Binary files /dev/null and b/app/frontend/images/login/treasure.png differ diff --git a/app/frontend/images/no_mail.png b/app/frontend/images/no_mail.png new file mode 100644 index 0000000..5d101ce Binary files /dev/null and b/app/frontend/images/no_mail.png differ diff --git a/app/frontend/js/click-to-copy.js b/app/frontend/js/click-to-copy.js new file mode 100644 index 0000000..94690dd --- /dev/null +++ b/app/frontend/js/click-to-copy.js @@ -0,0 +1,21 @@ +import $ from "jquery"; + +$('[data-copy-to-clipboard]').on('click', async function(e) { + const element = e.currentTarget; + const textToCopy = element.getAttribute('data-copy-to-clipboard'); + + try { + await navigator.clipboard.writeText(textToCopy); + + if (element.hasAttribute('aria-label')) { + const previousLabel = element.getAttribute('aria-label'); + element.setAttribute('aria-label', 'copied!'); + + setTimeout(() => { + element.setAttribute('aria-label', previousLabel); + }, 1000); + } + } catch (err) { + console.error('Failed to copy text: ', err); + } +}); diff --git a/app/frontend/js/components/instant_print_window.js b/app/frontend/js/components/instant_print_window.js new file mode 100644 index 0000000..f1b3ba0 --- /dev/null +++ b/app/frontend/js/components/instant_print_window.js @@ -0,0 +1,22 @@ +import {QZStatusBanner} from "./qz/status_banner"; +import {QZErrorBanner} from "./qz/error_banner"; +import {PrintButton} from "./qz/print_button"; +export function InstantPrintWindow() { + qz_state.pdf_url = this.pdf_url + return html`
+ ${$if( + use(qz_settings.printer, (p) => (qz_settings.printer === 'pick a printer...' || !qz_settings.printer)), + html` + + ` + )} + <${QZStatusBanner}/> + <${QZErrorBanner}/> + <${PrintButton}/> +
` +} \ No newline at end of file diff --git a/app/frontend/js/components/qz/dpi_picker.js b/app/frontend/js/components/qz/dpi_picker.js new file mode 100644 index 0000000..8afb07e --- /dev/null +++ b/app/frontend/js/components/qz/dpi_picker.js @@ -0,0 +1,21 @@ +function RadioButton() { + return html` + + ` +} + +export function DPIPicker() { + const DPI_OPTS = { + 203: "most common", + 300: "fancier!", + 305: "sometimes?" + } + return html` +
+ DPI + ${Object.entries(DPI_OPTS).map(([a, b]) => (h(RadioButton, {dpi: a, desc: b, settings: this.settings})))} +
` +} \ No newline at end of file diff --git a/app/frontend/js/components/qz/error_banner.js b/app/frontend/js/components/qz/error_banner.js new file mode 100644 index 0000000..79f79d6 --- /dev/null +++ b/app/frontend/js/components/qz/error_banner.js @@ -0,0 +1,13 @@ +export function QZErrorBanner() { + return html`${$if(use(qz_state.error), + html` + + `)}` +} \ No newline at end of file diff --git a/app/frontend/js/components/qz/print_button.js b/app/frontend/js/components/qz/print_button.js new file mode 100644 index 0000000..a43c37e --- /dev/null +++ b/app/frontend/js/components/qz/print_button.js @@ -0,0 +1,32 @@ +import {print} from "../../../entrypoints/qz"; + +export function PrintButton() { + this.disabled = false + this.printing = false; + useChange([qz_settings.printer, qz_state.status, this.printing], () => { + this.disabled = this.printing || qz_state.status !== 'connected' || qz_settings.printer === 'pick a printer...' || !qz_settings.printer + }) + + const handlePrintSuccess = () => { + // Find and click the mark printed button by ID + const markPrintedButton = document.getElementById('mark_printed'); + if (markPrintedButton) { + markPrintedButton.click(); + } + }; + + return html` + +
+ +
+ + ` +} + + diff --git a/app/frontend/js/components/qz/printer_picker.js b/app/frontend/js/components/qz/printer_picker.js new file mode 100644 index 0000000..14b4de9 --- /dev/null +++ b/app/frontend/js/components/qz/printer_picker.js @@ -0,0 +1,15 @@ +export function QZPrinterPicker() { + console.log() + return html` +
+ + +
+ ` +} diff --git a/app/frontend/js/components/qz/refresh_printers_button.js b/app/frontend/js/components/qz/refresh_printers_button.js new file mode 100644 index 0000000..3d734ec --- /dev/null +++ b/app/frontend/js/components/qz/refresh_printers_button.js @@ -0,0 +1,8 @@ +export function RefreshPrintersButton() { + return html` + + ` +} \ No newline at end of file diff --git a/app/frontend/js/components/qz/status_banner.js b/app/frontend/js/components/qz/status_banner.js new file mode 100644 index 0000000..d0233f4 --- /dev/null +++ b/app/frontend/js/components/qz/status_banner.js @@ -0,0 +1,19 @@ +const text = { + connecting: "connecting to QZ tray...", + connected: "connected to QZ tray!", + error: html`couldn't connect – do you need to install QZ tray?` +} + +const classes = { + connecting: "banner-warning", connected: "banner-success", error: "banner-danger" +} + +export function QZStatusBanner() { + + return html`${$if(use(qz_state.status, (s) => + (this.in_settings ? true : s !== 'connected') + ), html` +
classes[state])]}> + ${use(qz_state.status, (state) => text[state])} +
`)}
` +} \ No newline at end of file diff --git a/app/frontend/js/components/qz/test_print_button.js b/app/frontend/js/components/qz/test_print_button.js new file mode 100644 index 0000000..18b97a9 --- /dev/null +++ b/app/frontend/js/components/qz/test_print_button.js @@ -0,0 +1,17 @@ +export function TestPrintButton() { + this.disabled = false + this.printing = false + + useChange([qz_settings.printer, qz_state.status, this.printing], () => { + this.disabled = this.printing || qz_state.status !== 'connected' || qz_settings.printer === 'pick a printer...' || !qz_settings.printer + }) + return html` + +` +} \ No newline at end of file diff --git a/app/frontend/js/confetti.js b/app/frontend/js/confetti.js new file mode 100644 index 0000000..656d794 --- /dev/null +++ b/app/frontend/js/confetti.js @@ -0,0 +1,45 @@ +import {confetti} from "@tsparticles/confetti"; + +function fire_confetti() { + const count = 200, + defaults = { + origin: { y: 0.7 }, + }; + + function fire(particleRatio, opts) { + confetti( + Object.assign({}, defaults, opts, { + particleCount: Math.floor(count * particleRatio), + }) + ); + } + + fire(0.25, { + spread: 26, + startVelocity: 55, + }); + + fire(0.2, { + spread: 60, + }); + + fire(0.35, { + spread: 100, + decay: 0.91, + scalar: 0.8, + }); + + fire(0.1, { + spread: 120, + startVelocity: 25, + decay: 0.92, + scalar: 1.2, + }); + + fire(0.1, { + spread: 120, + startVelocity: 45, + }); +} + +window.fire_confetti = fire_confetti; \ No newline at end of file diff --git a/app/frontend/js/draggable-windows.js b/app/frontend/js/draggable-windows.js new file mode 100644 index 0000000..279aa84 --- /dev/null +++ b/app/frontend/js/draggable-windows.js @@ -0,0 +1,306 @@ +import $ from "jquery"; +import { initialize } from "@open-iframe-resizer/core"; +let currentZIndex = 1000; +// this is so bad i'm so sorry +// entirely a cursor invention +const WINDOW_GAP = 32; // pixels between windows +let windowCount = 0; // Track number of open windows + +function findBestPosition(windowWidth, windowHeight) { + // Positioning with carriage return + const padding = 20; + const windowGap = 20; + const rowPadding = 20; // Padding between rows + const zoom = 1.6; // Account for 160% zoom + const effectiveWidth = window.innerWidth / zoom; + + let currentX = padding; + let currentY = 90; + let currentRowWindows = []; + + // Go through each existing positioned window to calculate position + $('.window').each(function() { + const $existing = $(this); + const left = parseInt($existing.css('left')); + + // Only consider windows that have been positioned + if (!isNaN(left) && left >= 0) { + const existingWidth = $existing.outerWidth(); + const existingHeight = $existing.outerHeight(); + const existingId = $existing.attr('id'); + const nextX = currentX + existingWidth + windowGap; + + // Check if next window would go off screen OR if current window is backend-controls + const shouldWrap = (nextX + windowWidth > effectiveWidth - padding) || + (existingId === 'backend-controls'); + + if (shouldWrap) { + // Calculate height of current row based on tallest window + const rowHeight = currentRowWindows.length > 0 ? + Math.max(...currentRowWindows.map(w => w.height)) + rowPadding : 0; + + // Move to next row + currentX = padding; + currentY += rowHeight; + currentRowWindows = []; // Reset for new row + } else { + currentX = nextX; + } + + // Add this window to current row tracking + currentRowWindows.push({ height: existingHeight }); + } + }); + + const position = { + left: currentX, + top: currentY + }; + + console.log('findBestPosition called:', { windowWidth, windowHeight, effectiveWidth, position }); + return position; +} + +// Calculate positions for all initial windows first +const windowPositions = []; +const padding = 20; +const windowGap = 20; +const rowPadding = 20; // Padding between rows +const zoom = 1.6; // Account for 160% zoom + +let currentX = padding; +let currentY = 90; +let currentRowWindows = []; // Track windows in current row + +// Effective screen width accounting for zoom +const effectiveWidth = window.innerWidth / zoom; + +console.log('Screen width:', window.innerWidth, 'Zoom:', zoom, 'Effective width:', effectiveWidth, 'Available width:', effectiveWidth - padding); + +$('.window').each(function(index) { + if (window.innerWidth <= 768) { + return; + } + + const $window = $(this); + const windowWidth = $window.outerWidth(); + const windowHeight = $window.outerHeight(); + const windowId = $window.attr('id'); + + console.log(`Window ${index}: id=${windowId}, width=${windowWidth}, height=${windowHeight}, currentX=${currentX}, wouldEndAt=${currentX + windowWidth}`); + + // Check if this window would go off screen (accounting for zoom) OR if previous window was backend-controls + const shouldWrap = (index > 0 && currentX + windowWidth > effectiveWidth - padding) || + (index > 0 && $('.window').eq(index - 1).attr('id') === 'backend-controls'); + + if (shouldWrap) { + const reason = $('.window').eq(index - 1).attr('id') === 'backend-controls' ? 'after backend-controls' : 'screen width'; + console.log(`Window ${index} WRAPPING (${reason})`); + + // Calculate height of current row based on tallest window + const rowHeight = Math.max(...currentRowWindows.map(w => w.height)) + rowPadding; + console.log(`Current row max height: ${rowHeight - rowPadding}px, moving to Y: ${currentY + rowHeight}`); + + // Move to next row + currentX = padding; + currentY += rowHeight; + currentRowWindows = []; // Reset for new row + } else { + console.log(`Window ${index} fits on current row`); + } + + // Add this window to current row tracking + currentRowWindows.push({ height: windowHeight }); + + // Store position for this window + windowPositions[index] = { + left: currentX, + top: currentY + }; + + console.log('Calculated position for window', index, ':', windowPositions[index]); + + // Update currentX for next window + currentX += windowWidth + windowGap; +}); + +// Now apply all the calculated positions +$('.window').each(function(index) { + if (window.innerWidth <= 768) { + return; + } + const $window = $(this); + const $titleBar = $window.find('.title-bar'); + let isDragging = false; + let startX, startY, initialLeft, initialTop; + + // Apply the pre-calculated position + if (windowPositions[index]) { + $window.css({ + position: 'absolute', + left: `${windowPositions[index].left}px`, + top: `${windowPositions[index].top}px`, + 'z-index': currentZIndex + index + }); + + console.log('Applied position to window', index, ':', windowPositions[index]); + } + + function bringToFront() { + // Lower all other windows + $('.window').not($window).each(function() { + const currentZ = parseInt($(this).css('z-index')); + if (currentZ > currentZIndex) { + $(this).css('z-index', currentZ - 1); + } + }); + + // Bring this window to front + $window.css('z-index', currentZIndex + $('.window').length); + } + + function startDrag(e) { + bringToFront(); + console.log("startDrag"); + isDragging = true; + startX = e.clientX; + startY = e.clientY; + initialLeft = parseInt($window.css('left')); + initialTop = parseInt($window.css('top')); + + $(document).on('mousemove', drag); + $(document).on('mouseup', stopDrag); + } + + function drag(e) { + if (!isDragging) return; + + const zoom = 1.6; // 160% zoom + const deltaX = (e.clientX - startX) / zoom; + const deltaY = (e.clientY - startY) / zoom; + + $window.css({ + left: initialLeft + deltaX, + top: initialTop + deltaY + }); + } + + function stopDrag() { + isDragging = false; + $(document).off('mousemove', drag); + $(document).off('mouseup', stopDrag); + } + + $titleBar.on('mousedown', startDrag); + $window.on('mousedown', bringToFront); +}); + +function openIframeWindow(url, title) { + const windowId = `window-${Date.now()}`; + const $window = $(` +
+
+
${title}
+
+ + +
+
+
+ +
+
+ `); + + // Add to document first to get dimensions + $('body').append($window); + + // Get window dimensions + const windowWidth = $window.outerWidth(); + const windowHeight = $window.outerHeight(); + + // Find best position + const position = findBestPosition(windowWidth, windowHeight); + + // Position the window + $window.css({ + position: 'absolute', + left: `${position.left}px`, + top: `${position.top}px`, + 'z-index': currentZIndex + $('.window').length + }); + + makeWindowDraggable($window); + initialize({}, `#${windowId}-frame`); + + // Increment window count + windowCount++; +} + +function closeWindow(windowId) { + const $window = $(`#${windowId}`); + if ($window.length) { + $window.remove(); + windowCount--; + } +} + +function makeWindowDraggable($window) { + if (window.innerWidth <= 768) { + return; + } + const $titleBar = $window.find('.title-bar'); + let isDragging = false; + let startX, startY, initialLeft, initialTop; + + function bringToFront() { + // Lower all other windows + $('.window').not($window).each(function() { + const currentZ = parseInt($(this).css('z-index')); + if (currentZ > currentZIndex) { + $(this).css('z-index', currentZ - 1); + } + }); + + // Bring this window to front + $window.css('z-index', currentZIndex + $('.window').length); + } + + function startDrag(e) { + bringToFront(); + isDragging = true; + startX = e.clientX; + startY = e.clientY; + initialLeft = parseInt($window.css('left')); + initialTop = parseInt($window.css('top')); + + $(document).on('mousemove', drag); + $(document).on('mouseup', stopDrag); + } + + function drag(e) { + if (!isDragging) return; + + const zoom = 1.6; // 160% zoom + const deltaX = (e.clientX - startX) / zoom; + const deltaY = (e.clientY - startY) / zoom; + + $window.css({ + left: initialLeft + deltaX, + top: initialTop + deltaY + }); + } + + function stopDrag() { + isDragging = false; + $(document).off('mousemove', drag); + $(document).off('mouseup', stopDrag); + } + + $titleBar.on('mousedown', startDrag); + $window.on('mousedown', bringToFront); +} + +// Make the functions available globally +window.openIframeWindow = openIframeWindow; +window.closeWindow = closeWindow; \ No newline at end of file diff --git a/app/frontend/js/interactive-tables.js b/app/frontend/js/interactive-tables.js new file mode 100644 index 0000000..835ac1d --- /dev/null +++ b/app/frontend/js/interactive-tables.js @@ -0,0 +1,20 @@ +import $ from 'jquery'; + +$('table.interactive').each(function() { + const $table = $(this); + const highlightedClass = 'highlighted'; + + $table.on('click', function(event) { + const $target = $(event.target); + const $newlySelectedRow = $target.closest('tr'); + const $previouslySelectedRow = $table.find('tr.' + highlightedClass); + + if ($previouslySelectedRow.length) { + $previouslySelectedRow.removeClass(highlightedClass); + } + + if ($newlySelectedRow.length) { + $newlySelectedRow.toggleClass(highlightedClass); + } + }); +}); \ No newline at end of file diff --git a/app/frontend/js/turbo-confirm.js b/app/frontend/js/turbo-confirm.js new file mode 100644 index 0000000..32f125e --- /dev/null +++ b/app/frontend/js/turbo-confirm.js @@ -0,0 +1,9 @@ +import $ from "jquery"; +$(document).ready(function() { + $(document).on('submit', 'form[data-turbo-confirm]', function(e) { + const message = $(this).data('turbo-confirm'); + if (!confirm(message)) { + e.preventDefault(); + } + }); +}); \ No newline at end of file diff --git a/app/frontend/styles/app.scss b/app/frontend/styles/app.scss new file mode 100644 index 0000000..73f43f8 --- /dev/null +++ b/app/frontend/styles/app.scss @@ -0,0 +1,16 @@ +@use "piko.scss"; +@use "components/buttons" as *; +@use "components/banners" as *; +@use "components/badges" as *; +@use "components/warehouse" as *; +@use "components/return_addresses" as *; +@use "components/actions" as *; +@use "components/user_mention" as *; +@use "components/nav" as *; +@use "components/tabs" as *; +@use "components/tags" as *; +@use "components/tooltips" as *; +@use "components/marching_ants" as *; +@use "components/template_picker" as *; +@use "utils" as *; +@import "selectize"; \ No newline at end of file diff --git a/app/frontend/styles/basscss.css b/app/frontend/styles/basscss.css new file mode 100644 index 0000000..c2e378e --- /dev/null +++ b/app/frontend/styles/basscss.css @@ -0,0 +1,285 @@ +/*! Basscss | http://basscss.com | MIT License */ +.h0 { + font-size: 3rem; +} +.h1 { + font-size: 2rem; +} +.h2 { + font-size: 1.5rem; +} +.h3 { + font-size: 1.25rem; +} +.h4 { + font-size: 1rem; +} +.h5 { + font-size: 0.875rem; +} +.h6 { + font-size: 0.75rem; +} +/* +.font-family-inherit { + font-family: inherit; +} +.font-size-inherit { + font-size: inherit; +} +*/ +.text-decoration-none { + text-decoration: none; +} +.bold, +.semibold { + font-weight: 600; +} +.medium { + font-weight: 500; +} +.regular { + font-weight: 400; +} +.left-align { + text-align: left; +} +.center { + text-align: center; +} +.right-align { + text-align: right; +} +.justify { + text-align: justify; +} +.nowrap { + white-space: nowrap; +} +.line-height-1 { + line-height: 1; +} +.line-height-2 { + line-height: 1.125; +} +.line-height-3 { + line-height: 1.25; +} +.line-height-4 { + line-height: 1.5; +} +.no-select { + -webkit-user-select: none; + -moz-user-select: all; + -ms-user-select: all; + user-select: none; +} +.no-resize { + resize: none; +} +/* +.no-wrap { + white-space: nowrap; +} +*/ +.truncate { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.list-reset { + list-style: none; + padding-left: 0; +} +.overflow-visible { + overflow: visible; +} +.overflow-hidden { + overflow: hidden; +} +.overflow-scroll { + overflow: scroll; + -webkit-overflow-scrolling: touch; +} +.overflow-auto { + overflow: auto; + -webkit-overflow-scrolling: touch; +} +.clearfix:after, +.clearfix:before { + content: ' '; + display: table; +} +.clearfix:after { + clear: both; +} +.left { + float: left; +} +.right { + float: right; +} +.fit { + max-width: 100%; +} +.max-width-1 { + max-width: 24rem; +} +.max-width-2 { + max-width: 32rem; +} +.max-width-3 { + max-width: 48rem; +} +.max-width-4 { + max-width: 64rem; +} +.flex-auto { + -webkit-box-flex: 1; + -webkit-flex: 1 1 auto; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + min-width: 0; + min-height: 0; +} +@media (min-width: 32em) { + .md-left-align { + text-align: left; + } + .md-center { + text-align: center; + } + .md-left { + float: left; + } + .md-right { + float: right; + } + .md-flex { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + } + .md-flex-column { + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + } + .md-flex-row { + /* added as override */ + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + } + .md-justify-between { + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + } + .md-items-center { + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + -ms-grid-row-align: center; + align-items: center; + } + .md-items-baseline { + -webkit-box-align: baseline; + -webkit-align-items: baseline; + -ms-flex-align: baseline; + -ms-grid-row-align: baseline; + align-items: baseline; + } +} +@media (min-width: 64em) { + .lg-flex { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + } +} +.z1 { + z-index: 1; +} +.z2 { + z-index: 2; +} +.z3 { + z-index: 3; +} +.z4 { + z-index: 4; +} +.border { + border-style: solid; + border-width: 1px; +} +.border-top { + border-top-style: solid; + border-top-width: 1px; +} +.border-right { + border-right-style: solid; + border-right-width: 1px; +} +.border-bottom { + border-bottom-style: solid; + border-bottom-width: 1px; +} +.border-left { + border-left-style: solid; + border-left-width: 1px; +} +.border-none { + border: 0; +} +.hide { + position: absolute !important; + height: 1px; + width: 1px; + overflow: hidden; + clip: rect(1px, 1px, 1px, 1px); +} +@media (max-width: 32em) { + .xs-hide { + display: none !important; + } + .xs-capitalize { + text-transform: capitalize; + } +} +@media (min-width: 32em) and (max-width: 48em) { + .sm-hide { + display: none !important; + } +} +@media (min-width: 48em) and (max-width: 64em) { + .md-hide { + display: none !important; + } +} +@media (min-width: 64em) { + .lg-hide { + display: none !important; + } +} +.display-none { + display: none !important; +} +.display-none-not-important { + display: none; +} +.w-fit-content { + width: fit-content; +} +.h-fit-content { + height: fit-content; +} diff --git a/app/frontend/styles/colors.scss b/app/frontend/styles/colors.scss new file mode 100644 index 0000000..ff920ac --- /dev/null +++ b/app/frontend/styles/colors.scss @@ -0,0 +1,306 @@ +@use "sass:color"; + +$black: #1f2d3d; +$slate: #3c4858; +$muted: #8492a6; +$smoke: #e0e6ed; +$snow: #f9fafc; +$white: #fff; + +$red: #ec3750; +$orange: #ff8c37; +$yellow: #f1c40f; +$green: #33d6a6; +$green-dark: #1f8164; +$cyan: #5bc0de; +$blue: #338eda; +$purple: #a633d6; + +$dark: #17171d; +$darker: #121217; +$darkless: #252429; + +$palette: ( + primary: $red, + accent: $cyan, + info: $blue, + success: $green, + error: color.adjust(color.adjust($red, $lightness: -25%, $space: hsl), $saturation: -15%, $space: hsl), + ai: $purple, + warning: $orange, + pending: $yellow, + purple: $purple, + black: $black, + darker: $darker, + slate: $slate, + muted: $muted, + smoke: $smoke, + snow: $snow, + white: $white, + dark: $dark, +); + +@mixin gradient($color) { + background-color: map-get($palette, $color); + background-image: radial-gradient( + ellipse farthest-corner at top left, + color.adjust(color.adjust(map-get($palette, $color), $saturation: 15%, $space: hsl), $lightness: 15%, $space: hsl), + color.adjust(map-get($palette, $color), $saturation: 15%, $space: hsl) + ); +} + +html { + --error-bg:#fbf2f4; + --error-border:#eab4bc; + --error-fg:#b9031f; + --error-fg-strong:#78202e; + --warning-bg:#fffcf2; + --warning-border:#ffe69b; + --warning-fg:#ffc107; + --warning-fg-strong:#6a311c; + --success-bg:#f3f8f5; + --success-border:#a1caad; + --success-fg:#147b33; + --success-fg-strong:#285a37; + --info-bg:#f2f7fb; + --info-bg-selected:#cce0f1; + --info-border:#b2d1ea; + --info-fg:#0067b9; + --info-fg-strong:#1f5077; + --quote-fg-1: #2b497d; + --quote-bg-1: #e8ecf2; + --quote-fg-2: #1d3e0f; + --quote-bg-2: #e4f1df; + --quote-fg-3: #5c0a0a; + --quote-bg-3: #f7d4d4; + --quote-fg-4: #472215; + --quote-bg-4: #dbbeb3; + --quote-fg-5: #335f61; + --quote-bg-5: #e0e6e6; +} + + +@media (prefers-color-scheme: dark) { + + html { + --error-bg: #3b1017; + --error-border: #8b2535; + --error-fg: #dc818f; + --error-fg-strong: #e39aa5; + --warning-bg: #33290b; + --warning-border: #7f661c; + --warning-fg: #ffe083; + --warning-fg-strong: #ffecb4; + --success-bg: #142c1b; + --success-border: #285a37; + --success-fg: #8abd99; + --success-fg-strong: #b9d8c2; + --info-bg: #0f273b; + --info-bg-selected: #436075; + --info-border: #436075; + --info-fg: #b4d9f3; + --info-fg-strong: #d2e8f7; + --quote-fg-1: #b3cbff; + --quote-bg-1: #373a3f; + --quote-fg-2: #bee3aa; + --quote-bg-2: #313b2d; + --quote-fg-3: #ffc4b3; + --quote-bg-3: #55393a; + --quote-fg-4: #ffd3c0; + --quote-bg-4: #5e473e; + --quote-fg-5: #9ac9ca; + --quote-bg-5: #393d3e; + } + +} +html { + --checkbox-true: #08e54c; + --checkbox-false: #ff002a; +} + +// Colors +:root { + --color-primary: #4a9eff; + --color-primary-rgb: 74, 158, 255; + --color-primary-hover: #3c7fcc; + --color-primary-dark: #2563eb; + --color-secondary: #6c757d; + --color-secondary-rgb: 108, 117, 125; + --color-secondary-hover: #5a6268; + --color-success: #33d6a6; + --color-success-rgb: 51, 214, 166; + --color-danger: #dc3545; + --color-danger-rgb: 220, 53, 69; + --color-warning: #ffc107; + --color-warning-rgb: 255, 193, 7; + --color-info: #17a2b8; + --color-info-rgb: 23, 162, 184; + + // Neutrals + --color-white: #ffffff; + --color-gray-50: #f9fafb; + --color-gray-100: #f9fafc; + --color-gray-200: #e0e6ed; + --color-gray-300: #d1d5db; + --color-gray-400: #9ca3af; + --color-gray-500: #6b7280; + --color-gray-600: #4b5563; + --color-gray-700: #374151; + --color-gray-800: #1f2937; + --color-gray-900: #111827; + --color-black: #000000; + + // Text colors + --color-text: var(--color-gray-900); + --color-text-secondary: var(--color-gray-600); + --color-text-muted: var(--color-gray-500); + --color-heading: var(--color-gray-900); + --text-primary: var(--color-text); + --text-secondary: var(--color-text-secondary); + + // Border colors + --color-border: var(--color-gray-200); + --border-color: var(--color-border); + + // Background colors + --color-bg: var(--color-white); + --color-card-bg: var(--color-white); + --color-panel-bg: var(--color-gray-50); + --bg-default: var(--color-bg); + --bg-subtle: var(--color-gray-100); + --bg-subtle-hover: var(--color-gray-200); + --bg-card: var(--color-card-bg); + + // Component specific + --tab-bg: var(--color-gray-100); + --active-indicator: var(--color-primary); + + // Status-specific colors + --color-danger-light: #fecaca; + --color-danger-50: #fef2f2; + + // Shadows + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + + // Spacing + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 0.75rem; + --space-4: 1rem; + --space-5: 1.25rem; + --space-6: 1.5rem; + --space-8: 2rem; + --space-10: 2.5rem; + --space-12: 3rem; + + // Typography + --font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-base: 1rem; + --text-md: 1.125rem; + --text-lg: 1.125rem; + --text-xl: 1.25rem; + --text-2xl: 1.5rem; + --text-3xl: 1.875rem; + --font-size-xs: var(--text-xs); + --font-size-sm: var(--text-sm); + --font-size-base: var(--text-base); + --font-size-lg: var(--text-lg); + --font-size-xl: var(--text-xl); + --font-size-2xl: var(--text-2xl); + + // Font weights + --font-normal: 400; + --font-medium: 500; + --font-semibold: 600; + --font-bold: 700; + --font-weight-normal: var(--font-normal); + --font-weight-medium: var(--font-medium); + --font-weight-bold: var(--font-bold); + + // Border radius + --radius-sm: 0.25rem; + --radius-md: 0.375rem; + --radius-lg: 0.5rem; + --radius-xl: 0.75rem; + + // Transitions + --transition: 200ms ease; + --transition-fast: 150ms ease; + --transition-base: 200ms ease; + --transition-slow: 300ms ease; +} + +// Dark mode overrides +@media (prefers-color-scheme: dark) { + :root { + // Base colors (dark mode) + --color-primary: #4a9eff; + --color-primary-hover: #6ab0ff; + + // Neutrals + --color-gray-50: #242424; + --color-gray-100: #1a1a1a; + --color-gray-200: #3a3a48; + --color-gray-300: #4d4d5c; + --color-gray-400: #717181; + --color-gray-500: #8f8f9e; + --color-gray-600: #a0a0b0; + --color-gray-700: #c5c5d1; + --color-gray-800: #e0e0ea; + --color-gray-900: #f2f2f8; + + // Text colors + --color-text: var(--color-gray-900); + --color-text-secondary: var(--color-gray-700); + --color-text-muted: var(--color-gray-600); + --color-heading: var(--color-gray-900); + --text-primary: var(--color-text); + --text-secondary: var(--color-text-secondary); + + // Border colors + --color-border: var(--color-gray-200); + --border-color: var(--color-border); + + // Background colors + --color-bg: #121212; + --color-card-bg: #1f1f1f; + --color-panel-bg: #2d2d2d; + --bg-default: var(--color-bg); + --bg-subtle: #252530; + --bg-subtle-hover: #2f2f3d; + --bg-card: var(--color-card-bg); + + // Component specific + --tab-bg: #252530; + + // Status-specific colors + --color-danger-light: #7f1d1d; + --color-danger-50: #450a0a; + } +} + +// Danger Zone Component +.danger-zone { + background-color: var(--color-danger-50); + border: 1px solid var(--color-danger-light); + border-radius: var(--radius-lg); + padding: var(--space-4); + margin-top: var(--space-6); +} + +.text-danger { + color: var(--color-danger); +} + +@mixin gradient($color) { + background-color: map-get($palette, $color); + background-image: radial-gradient( + ellipse farthest-corner at top left, + lighten(saturate(map-get($palette, $color), 15%), 15%), + saturate(map-get($palette, $color), 15%) + ); +} \ No newline at end of file diff --git a/app/frontend/styles/components/_buttons.scss b/app/frontend/styles/components/_buttons.scss new file mode 100644 index 0000000..5a17b8e --- /dev/null +++ b/app/frontend/styles/components/_buttons.scss @@ -0,0 +1,298 @@ +@import "../colors"; + +button { + width: auto !important; +} +input[type='submit'], +.btn { + -webkit-appearance: none; + align-items: center; + border: 0; + border-radius: 0.75rem; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.125); + @include gradient(info); + color: $white; + cursor: pointer; + display: inline-flex; + font-size: 1.125rem; + font-weight: 600; + justify-content: center; + letter-spacing: 0; + line-height: 1.5; + overflow: hidden; + padding: 0.5rem 1.25rem; + position: relative; + text-align: center; + text-decoration: none; + transform: scale(1); + transition: + transform 0.125s ease-in-out, + box-shadow 0.25s ease-in-out; + user-select: none; + + &:hover, + &:focus, + &[aria-expanded='true'] { + color: $white; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1875); + // transform: scale(1.0625); + } + + &.disabled, + &[disabled='disabled'], + &:disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; + } + + svg { + width: 24px; + height: 24px; + margin-left: -4px; + margin-right: 6px; + } + + &.primary { + @include gradient(primary); + } + + &.smoke { + background-color: lighten(map-get($palette, smoke), 3%); + color: darken(map-get($palette, smoke), 50%) !important; + + html[data-dark='true'] & { + background-color: darken(map-get($palette, slate), 13%); + color: lighten(map-get($palette, slate), 50%) !important; + } + } + + &.muted { + @include gradient(muted); + } + + &.warning { + @include gradient(warning); + } + + &.purple { + @include gradient(purple); + } + + &.ai { + @include gradient(ai); + } + + &.success { + @include gradient(success); + color: darken(map-get($palette, success), 33.3%) !important; + } + + &.accent { + @include gradient(accent); + color: darken(map-get($palette, accent), 33.3%) !important; + } + + &.danger, + &.btn--destroy { + @include gradient(error); + } + + &.outlined { + background: transparent; + box-shadow: none; + border: 2px solid map-get($palette, info); + color: map-get($palette, info); + + &:hover, + &:focus, + &[aria-expanded='true'] { + box-shadow: none; + background: rgba(map-get($palette, info), 0.1); + } + + &.primary { + border-color: map-get($palette, primary); + color: map-get($palette, primary); + &:hover, &:focus, &[aria-expanded='true'] { + background: rgba(map-get($palette, primary), 0.1); + } + } + + &.info { + border-color: map-get($palette, info); + color: map-get($palette, info); + &:hover, &:focus, &[aria-expanded='true'] { + background: rgba(map-get($palette, info), 0.1); + } + } + + &.warning { + border-color: map-get($palette, warning); + color: map-get($palette, warning); + &:hover, &:focus, &[aria-expanded='true'] { + background: rgba(map-get($palette, warning), 0.1); + } + } + + &.success { + border-color: map-get($palette, success); + color: map-get($palette, success) !important; + &:hover, &:focus, &[aria-expanded='true'] { + background: rgba(map-get($palette, success), 0.1); + } + } + + &.danger { + border-color: map-get($palette, error); + color: map-get($palette, error); + &:hover, &:focus, &[aria-expanded='true'] { + background: rgba(map-get($palette, error), 0.1); + } + } + + &.accent { + border-color: map-get($palette, accent); + color: map-get($palette, accent); + &:hover, &:focus, &[aria-expanded='true'] { + background: rgba(map-get($palette, accent), 0.1); + } + } + + &.purple { + border-color: map-get($palette, purple); + color: map-get($palette, purple); + &:hover, &:focus, &[aria-expanded='true'] { + background: rgba(map-get($palette, purple), 0.1); + } + } + + &.ai { + border-color: map-get($palette, ai); + color: map-get($palette, ai); + &:hover, &:focus, &[aria-expanded='true'] { + background: rgba(map-get($palette, ai), 0.1); + } + } + + &.muted { + border-color: map-get($palette, muted); + color: map-get($palette, muted); + &:hover, &:focus, &[aria-expanded='true'] { + background: rgba(map-get($palette, muted), 0.1); + } + } + } +} + +.btn--form-option { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 1rem; + border-radius: 0.75rem; + text-align: center; + line-height: 1.25; + color: map-get($palette, info); + background-color: rgba(map-get($palette, accent), 0.25); + + &:hover, + &:focus { + color: map-get($palette, info); + } + + html[data-dark='true'] & { + color: map-get($palette, white); + } +} + +.btn--form-option--disabled { + background-color: rgba(map-get($palette, accent), 0.12); + color: rgba(map-get($palette, info), 0.7); + + html[data-dark='true'] & { + color: rgba(map-get($palette, white), 0.7); + } + + cursor: default; + pointer-events: none; + user-select: none; +} + +.btn-small { + font-size: 0.875rem; + font-weight: 600; + line-height: 1.75; + padding: 0.25rem 1rem; +} + +.btn-tiny { + padding: 0rem 0.6rem; + border-radius: 0.5rem; + font-size: 0.875rem; + font-weight: 600; + line-height: 1.75; +} + +form .flex-auto + input[type='submit'] { + height: 2.5rem; +} + +.btn-group { + display: inline-flex; + align-items: center; + + @media (max-width: 32em) { + .btn { + margin-right: 8px; + } + + &:not(.btn-group--no-wrap) { + flex-wrap: wrap; + + .btn { + margin-bottom: 8px; + } + } + + &.center { + justify-content: center; + .btn { + margin: 8px; + } + } + } + + @media (min-width: 32em) { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.125); + border-radius: 0.75rem; + + .btn:first-child { + border-radius: 0.75rem 0 0 0.75rem; + } + .btn { + border-radius: 0; + box-shadow: none; + margin: 0; + + &:hover, + &:focus { + z-index: 2; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.125); + } + } + .btn:last-child { + border-radius: 0 0.75rem 0.75rem 0; + } + } +} + + +a.btn { + display: inline-flex; + align-items: center; + line-height: 1.5; + font-size: 1.125rem; + font-weight: 600; +} \ No newline at end of file diff --git a/app/frontend/styles/components/_tabs.scss b/app/frontend/styles/components/_tabs.scss new file mode 100644 index 0000000..cc2a760 --- /dev/null +++ b/app/frontend/styles/components/_tabs.scss @@ -0,0 +1,81 @@ +.tabs { + display: flex; + gap: var(--space-1); + list-style: none; + padding: 0; + margin: 0; +// border-bottom: 1px solid var(--color-border); +// background: var(--color-bg); + padding: var(--space-2) var(--space-2) 0; + + &__item { + margin: 0; + } + + &__link { + display: block; + padding: var(--space-3) var(--space-6); + text-decoration: none; + color: var(--color-text); + position: relative; + transition: var(--transition); + border: 1px solid var(--color-border); + border-bottom: none; + border-radius: var(--radius-lg) var(--radius-lg) 0 0; + background: var(--bg-subtle); + margin-bottom: -1px; + font-family: var(--font-family); + font-size: var(--text-base); + font-weight: var(--font-weight-medium); + box-shadow: var(--shadow-sm); + + &:hover { + color: var(--color-primary); + background: var(--bg-subtle-hover); + transform: translateY(-1px); + } + + &--active { + color: var(--color-primary); + background: var(--color-bg); + border-color: var(--color-border); + border-bottom-color: var(--color-bg); + box-shadow: var(--shadow-sm); + + &:hover { + color: var(--color-primary); + transform: none; + } + } + } +} + +// Dark theme support +[data-theme="dark"] { + .tabs { + background: var(--color-bg); + border-bottom-color: var(--color-gray-200); + + &__link { + color: var(--color-text); + background: var(--bg-subtle); + border-color: var(--color-gray-200); + + &:hover { + color: var(--color-primary); + background: var(--bg-subtle-hover); + } + + &--active { + color: var(--color-primary); + background: var(--color-bg); + border-color: var(--color-gray-200); + border-bottom-color: var(--color-bg); + + &:hover { + color: var(--color-primary); + } + } + } + } +} \ No newline at end of file diff --git a/app/frontend/styles/components/_template_picker.scss b/app/frontend/styles/components/_template_picker.scss new file mode 100644 index 0000000..30ce30d --- /dev/null +++ b/app/frontend/styles/components/_template_picker.scss @@ -0,0 +1,126 @@ +.template-picker { + --template-picker-gap: var(--space-3); + --template-picker-border-radius: var(--radius-lg); + --template-picker-padding: var(--space-4); + --template-picker-item-size: 160px; + + padding: var(--template-picker-padding); + background: var(--bg-card); + border-radius: var(--template-picker-border-radius); + box-shadow: var(--shadow-md); + + &__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--space-4); + } + + &__title { + font-size: var(--text-lg); + font-weight: var(--font-weight-semibold); + color: var(--color-text); + margin: 0; + } + + &__mode-toggle { + .template-picker__mode-button { + padding: var(--space-1) var(--space-3); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + background: var(--bg-subtle); + color: var(--color-text); + font-size: var(--text-sm); + font-weight: var(--font-weight-medium); + cursor: pointer; + transition: var(--transition); + + &:hover { + background: var(--bg-subtle-hover); + border-color: var(--color-border); + } + } + } + + &__grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(var(--template-picker-item-size), 1fr)); + gap: var(--template-picker-gap); + margin-bottom: var(--space-4); + } + + &__item { + position: relative; + border: 2px solid transparent; + border-radius: var(--template-picker-border-radius); + cursor: pointer; + transition: var(--transition); + background: var(--bg-subtle); + overflow: hidden; + + &:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); + } + + &.selected { + border-color: var(--color-primary); + background: var(--bg-subtle-hover); + + .template-picker__order { + opacity: 1; + } + } + } + + &__preview { + // aspect-ratio: 1; + overflow: hidden; + background: var(--bg-card); + border-radius: calc(var(--template-picker-border-radius) - 2px); + } + + &__image { + width: 100%; + height: 100%; + object-fit: contain; + padding: var(--space-2); + } + + &__label { + padding: var(--space-2); + text-align: center; + font-size: var(--text-xs); + color: var(--color-text); + background: var(--bg-card); + } + + &__order { + position: absolute; + top: var(--space-1); + right: var(--space-1); + width: 1.25rem; + height: 1.25rem; + background: var(--color-primary); + color: var(--color-white); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: var(--text-xs); + font-weight: var(--font-weight-semibold); + opacity: 0; + transition: var(--transition); + } + + &__footer { + padding-top: var(--space-3); + border-top: 1px solid var(--color-border); + } + + &__selected-count { + font-size: var(--text-sm); + color: var(--color-text-secondary); + text-align: center; + } +} diff --git a/app/frontend/styles/components/_tooltips.scss b/app/frontend/styles/components/_tooltips.scss new file mode 100644 index 0000000..5733341 --- /dev/null +++ b/app/frontend/styles/components/_tooltips.scss @@ -0,0 +1,84 @@ +@use "../colors"; + + +.tooltipped { + position: relative; +} + +@media (min-width: 56em) { + .tooltipped { + &:after { + background-color: rgba(map-get(colors.$palette, black), 0.875); + border-radius: 0.5rem; + box-shadow: + 0 0 2px 0 rgba(0, 0, 0, 0.0625), + 0 4px 8px 0 rgba(0, 0, 0, 0.125); + color: map-get(colors.$palette, white); + content: attr(aria-label); + font-size: 0.875rem; + font-weight: 500; + height: min-content; + letter-spacing: 0; + line-height: 1.375; + max-width: 16rem; + min-height: 1.25rem; + opacity: 0; + padding: 0.25rem 0.75rem; + pointer-events: none; + position: absolute; + right: 100%; + text-align: center; + transform: translateY(50%); + transition: 0.125s all ease-in-out; + width: max-content; + z-index: 1; + + &.tooltipped--lg { + max-width: 24rem; + } + } + + &:hover:after, + &:active:after, + &:focus:after { + opacity: 1; + z-index: 9; + backdrop-filter: blur(2px); + } + } + + .tooltipped--e:after { + left: 100%; + bottom: 50%; + right: 0; + margin-left: 0.5rem; + transform: translateY(50%); + } + + .tooltipped--w:after { + right: 100%; + bottom: 50%; + margin-right: 0.5rem; + transform: translateY(50%); + } + + .tooltipped--n:after { + right: 50%; + bottom: 100%; + margin-bottom: 0.5rem; + transform: translateX(50%); + } + + .tooltipped--s:after { + right: 50%; + top: 100%; + margin-top: 0.5rem; + transform: translateX(50%); + } + + .tooltipped--xl { + &:after { + max-width: none; + } + } +} \ No newline at end of file diff --git a/app/frontend/styles/components/actions.scss b/app/frontend/styles/components/actions.scss new file mode 100644 index 0000000..7ccdf52 --- /dev/null +++ b/app/frontend/styles/components/actions.scss @@ -0,0 +1,5 @@ +.actions { + display: flex; + gap: var(--pico-spacing); + margin-bottom: var(--pico-spacing); +} \ No newline at end of file diff --git a/app/frontend/styles/components/badges.scss b/app/frontend/styles/components/badges.scss new file mode 100644 index 0000000..aa8c995 --- /dev/null +++ b/app/frontend/styles/components/badges.scss @@ -0,0 +1,93 @@ +@use "sass:color"; +@use "sass:map"; +@use "../colors"; + +.badge { + display: inline-flex; + flex-shrink: 0; + align-items: center; + background-color: map.get(colors.$palette, primary); + color: colors.$white; + border-radius: 9999px; + gap: 0.25rem; + font-size: 0.75rem; + font-weight: 500; + letter-spacing: 0.01em; + font-variant-numeric: tabular-nums; + line-height: 1.125; + margin-left: 1rem; + padding: 0.25rem 0.625rem; + + &.success { + background-color: rgba(map.get(colors.$palette, success), 0.2); + color: color.adjust(map.get(colors.$palette, success), $lightness: -10%); + + @media (prefers-color-scheme: dark) { + background-color: rgba(map.get(colors.$palette, success), 0.125); + color: map.get(colors.$palette, success); + } + } + &.pending { + background-color: rgba(map.get(colors.$palette, pending), 0.125); + color: map.get(colors.$palette, pending); + } + &.warning { + background-color: rgba(map.get(colors.$palette, warning), 0.125); + color: map.get(colors.$palette, warning); + } + &.purple { + background-color: rgba(map.get(colors.$palette, purple), 0.125); + color: map.get(colors.$palette, purple); + } + &.ai { + background-color: rgba(map.get(colors.$palette, ai), 0.125); + color: map.get(colors.$palette, ai); + } + &.error { + background-color: rgba(map.get(colors.$palette, error), 0.125); + color: map.get(colors.$palette, error); + + @media (prefers-color-scheme: dark) { + background-color: rgba(map.get(colors.$palette, error), 0.3); + color: color.adjust(color.adjust(map.get(colors.$palette, error), $saturation: 20%), $lightness: 40%); + } + } + &.info { + background-color: rgba(map.get(colors.$palette, info), 0.125); + color: map.get(colors.$palette, info); + } + &.bg-accent { + background-color: rgba(map.get(colors.$palette, accent), 0.125); + color: map.get(colors.$palette, accent); + } + &.bg-muted { + background-color: rgba(map.get(colors.$palette, muted), 0.125); + color: map.get(colors.$palette, slate); + } + &.muted { + background-color: map.get(colors.$palette, muted); + color: colors.$white; + } + + &.badge-large { + padding: 0.3rem 1rem; + gap: 0.5rem; + } + + &.tx-tag { + font-weight: normal; + background-color: transparent; + color: map.get(colors.$palette, slate); + border: 1px solid; + border-color: rgba(0, 0, 0, 0.125); + border-width: 0.5px; + max-width: 20rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .heading > & { + margin-left: 0; + } +} \ No newline at end of file diff --git a/app/frontend/styles/components/banners.scss b/app/frontend/styles/components/banners.scss new file mode 100644 index 0000000..ad6eb63 --- /dev/null +++ b/app/frontend/styles/components/banners.scss @@ -0,0 +1,36 @@ +.banner { + border: 1px solid; + border-radius: 8px; + padding: var(--space-3) var(--space-4); + margin-bottom: var(--space-4); + font-weight: var(--font-weight-medium); +} + +.banner > svg { + height: 1.25rem; + width: 1.25rem; +} + +.banner-warning { + background: var(--warning-bg); + border-color: var(--warning-border); + color: var(--warning-fg-strong); +} + +.banner-danger { + background: var(--error-bg); + border-color: var(--error-border); + color: var(--error-fg-strong); +} + +.banner-info { + background: var(--info-bg); + border-color: var(--info-border); + color: var(--info-fg-strong); +} + +.banner-success { + background: var(--success-bg); + border-color: var(--success-border); + color: var(--success-fg-strong); +} \ No newline at end of file diff --git a/app/frontend/styles/components/marching_ants.scss b/app/frontend/styles/components/marching_ants.scss new file mode 100644 index 0000000..c2d7aa8 --- /dev/null +++ b/app/frontend/styles/components/marching_ants.scss @@ -0,0 +1,16 @@ +.breathe-green { + border: 2px dashed rgb(34, 139, 34); + animation: breathe 3s ease-in-out infinite; +} + +@keyframes breathe { + 0% { + border-color: rgba(34, 139, 34, 0.4); + } + 50% { + border-color: rgba(34, 139, 34, 1); + } + 100% { + border-color: rgba(34, 139, 34, 0.4); + } +} \ No newline at end of file diff --git a/app/frontend/styles/components/nav.scss b/app/frontend/styles/components/nav.scss new file mode 100644 index 0000000..0c03ad0 --- /dev/null +++ b/app/frontend/styles/components/nav.scss @@ -0,0 +1,106 @@ +.nav { + background: var(--card-background-color); + padding: 1rem; + height: 100vh; + width: 250px; + position: fixed; + left: 0; + top: 0; + z-index: 100; + transition: transform 0.3s ease; + + @media (max-width: 768px) { + transform: translateX(-100%); + + &.active { + transform: translateX(0); + } + } + + nav { + ul { + padding: 0; + margin: 0; + list-style: none; + } + + li { + margin: 0; + } + + a { + display: block; + padding: 0.5rem; + color: var(--h1-color); + text-decoration: none; + + &:hover { + color: var(--primary); + } + + &.active { + color: #fff; + background-color: #bf9631; + color: #000; + font-weight: 500; + } + } + + details { + margin-bottom: 0; + + summary { + padding: 0.5rem 0; + cursor: pointer; + color: var(--h1-color); + font-weight: 500; + + &:hover { + color: var(--primary); + } + } + + ul { + padding-left: 1.25rem; + } + } + + hr { + margin: 1rem 0; + } + } +} + +.nav-toggle { + position: fixed; + bottom: 1rem; + right: 1rem; + z-index: 99; + display: none; + padding: 0.5rem; + border-radius: 50%; + background: var(--primary); + border: none; + color: white; + box-shadow: var(--card-box-shadow); + + svg { + display: block; + } + + @media (max-width: 768px) { + display: block; + } +} + +// Ensure main content is pushed to the right +main { + margin-top: 1.5rem; + margin-bottom: 1rem; + margin-right: 1.5rem; + margin-left: 250px; + + @media (max-width: 768px) { + margin-left: 0; + } +} diff --git a/app/frontend/styles/components/return_addresses.scss b/app/frontend/styles/components/return_addresses.scss new file mode 100644 index 0000000..15cfd09 --- /dev/null +++ b/app/frontend/styles/components/return_addresses.scss @@ -0,0 +1,15 @@ +.return-addresses-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + column-gap: var(--pico-spacing); + width: 100%; + + .card { + min-width: 0; + } + + @media (max-width: 768px) { + grid-template-columns: 1fr; + gap: var(--pico-spacing); + } +} \ No newline at end of file diff --git a/app/frontend/styles/components/tags.scss b/app/frontend/styles/components/tags.scss new file mode 100644 index 0000000..286826c --- /dev/null +++ b/app/frontend/styles/components/tags.scss @@ -0,0 +1,59 @@ +@use "sass:color"; +@use "sass:map"; +@use "../colors"; + +.tags { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin: 0; + padding: 0; + list-style: none; + + &.tags-compact { + gap: 0.25rem; + } + + &.tags-vertical { + flex-direction: column; + align-items: flex-start; + } +} + +.tag { + display: inline-flex; + align-items: center; + background-color: rgba(map.get(colors.$palette, info), 0.1); + color: map.get(colors.$palette, info); + border-radius: 4px; + gap: 0.25rem; + font-size: 0.75rem; + font-weight: 500; + line-height: 1.125; + padding: 0.25rem 0.625rem; + transition: background-color 0.2s ease; + + a { + color: var(--pico-color-azure-600); + text-decoration: none; + + &:hover { + color: var(--pico-color-azure-700); + text-decoration: underline; + } + } + + &:hover { + background-color: rgba(map.get(colors.$palette, info), 0.15); + } + + &.tag-large { + padding: 0.375rem 0.75rem; + font-size: 0.875rem; + } + + &.tag-small { + padding: 0.125rem 0.375rem; + font-size: 0.6875rem; + } +} \ No newline at end of file diff --git a/app/frontend/styles/components/user_mention.css b/app/frontend/styles/components/user_mention.css new file mode 100644 index 0000000..aae83c1 --- /dev/null +++ b/app/frontend/styles/components/user_mention.css @@ -0,0 +1,35 @@ +.avatar { + border-radius: 50%; + margin-right: 8px; + aspect-ratio: 1; +} + +.current-user { + font-weight: bold; + display: flex; + align-items: center; +} + +aside .current-user { + margin-bottom: 1rem; +} + +.current-user .avatar { + border: 2px solid #00dded; +} + + +.avatar:hover { + animation: spin 1s linear infinite; + animation-delay: 3s; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/app/frontend/styles/components/warehouse.scss b/app/frontend/styles/components/warehouse.scss new file mode 100644 index 0000000..6f2d04b --- /dev/null +++ b/app/frontend/styles/components/warehouse.scss @@ -0,0 +1,16 @@ +.warehouse-sku-card { + width: 100%; + margin-bottom: 1rem; +} + +// Grid container for SKU cards +.warehouse-skus-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + padding: 1rem; + + @media (max-width: 768px) { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/app/frontend/styles/login.scss b/app/frontend/styles/login.scss new file mode 100644 index 0000000..de647e2 --- /dev/null +++ b/app/frontend/styles/login.scss @@ -0,0 +1,31 @@ +@use 'colors' as *; +// @use "components/base"; +@use 'components/banners'; + + +html { + color-scheme: light dark; +} + +#treasure { + image-rendering: optimizeSpeed; + image-rendering: -moz-crisp-edges; + image-rendering: -o-crisp-edges; + image-rendering: -webkit-crisp-edges; + image-rendering: pixelated; + -ms-interpolation-mode: nearest-neighbor; + width: 100% +} + +@media (prefers-color-scheme: dark) { + #treasure { + filter: invert(1); + } +} + +body { + max-width: 35em; + width: 90%; + margin: 2em auto; +} + diff --git a/app/frontend/styles/nav.css b/app/frontend/styles/nav.css new file mode 100644 index 0000000..e69de29 diff --git a/app/frontend/styles/piko.scss b/app/frontend/styles/piko.scss new file mode 100644 index 0000000..325a3ce --- /dev/null +++ b/app/frontend/styles/piko.scss @@ -0,0 +1,43 @@ +:root { + --pico-font-family-sans-serif: Inter, system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, Helvetica, Arial, "Helvetica Neue", sans-serif, var(--pico-font-family-emoji) !important; + --pico-font-size: 16px !important; + /* Original: 100% */ + --pico-line-height: 1.25 !important; + /* Original: 1.5 */ + --pico-form-element-spacing-vertical: 0.5rem !important; + /* Original: 1rem */ + --pico-form-element-spacing-horizontal: 1.0rem !important; + /* Original: 1.25rem */ + --pico-border-radius: 0.375rem !important; + /* Original: 0.25rem */ +} + +h1, +h2, +h3, +h4, +h5, +h6 { + --pico-font-weight: 600; + /* Original: 700 */ +} + +article { + border: 1px solid var(--pico-muted-border-color); + /* Original doesn't have a border */ + border-radius: calc(var(--pico-border-radius) * 2); + /* Original: var(--pico-border-radius) */ +} + +article>footer { + border-radius: calc(var(--pico-border-radius) * 2); + /* Original: var(--pico-border-radius) */ +} + +$theme-color: "amber"; +// orange, pink, pumpkin, purple, red, sand, slate, violet, yellow, zinc + +pre.debug_dump kbd { + background-color: unset !important; +} +@import "@picocss/pico/scss/pico"; \ No newline at end of file diff --git a/app/frontend/styles/public.scss b/app/frontend/styles/public.scss new file mode 100644 index 0000000..5fbc394 --- /dev/null +++ b/app/frontend/styles/public.scss @@ -0,0 +1,96 @@ +@use "components/tooltips" as *; +@use "components/banners" as *; +@use "utils" as *; + +// Variables +$spacing-sm: 2px; +$spacing-md: 8px; +$spacing-lg: 2em; +$icon-size-sm: 14px; +$icon-size-md: 32px; +$admin-tool-border: 2px dashed rgb(255, 140, 0); +$link-color: blue; +$link-color-highlighted: white; + +html { + color-scheme: light dark; + zoom: 160%; +} + +input { + color: black; +} + +body:not(.framed) { + margin: $spacing-lg; +} + +a.open-link { + color: $link-color !important; + + .highlighted > td > & { + color: $link-color-highlighted !important; + } +} + +.dialog-icon { + width: $icon-size-md; + display: inline; + float: left; + margin-right: $spacing-md; +} + +img { + image-rendering: pixelated; +} + +.title-icon { + width: $icon-size-sm; + height: $icon-size-sm; + position: absolute; + margin-right: $spacing-md; +} + +.title-bar { + &.iconed > .title-bar-text { + margin-left: 24px; + } +} + +.admin-tool { + border: $admin-tool-border; + border-radius: 0; + padding: $spacing-sm; + + &.span { + border-radius: 0; + padding: 3px $spacing-sm; + } +} + +.window { + position: absolute; +} + +@media (max-width: 768px) { + .window { + position: static !important; + transform: none !important; + left: auto !important; + top: auto !important; + margin-bottom: $spacing-lg; + margin-right: 5px; + } + html:not(.hframed) { + zoom: 100%; + } +} + +.framed { + font-family: "Pixelated MS Sans Serif", Arial; + zoom: 62.5%; +} + +iframe { + border: none; +} \ No newline at end of file diff --git a/app/frontend/styles/selectize.scss b/app/frontend/styles/selectize.scss new file mode 100644 index 0000000..7c806bb --- /dev/null +++ b/app/frontend/styles/selectize.scss @@ -0,0 +1,133 @@ +@use "sass:color"; +@use "sass:map"; +@use "colors"; + +// Variables +$tag-padding: ( + default: 0.25rem 0.625rem, + large: 0.375rem 0.75rem, + small: 0.125rem 0.375rem +); + +$tag-font-size: ( + default: 0.75rem, + large: 0.875rem, + small: 0.6875rem +); + +// Mixins +@mixin form-element-base { + background-color: var(--pico-form-element-background-color) !important; + border: var(--pico-border-width) solid var(--pico-form-element-border-color) !important; + border-radius: var(--pico-border-radius) !important; + padding: var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal) !important; + box-shadow: none !important; + outline: none !important; +} + +@mixin focus-state { + border-color: var(--pico-primary) !important; + box-shadow: 0 0 0 var(--pico-outline-width) var(--pico-primary-focus) !important; +} + +@mixin hover-transition { + transition: background-color var(--pico-transition) !important; +} + +// Base styles +.selectize-input { + @include form-element-base; + font-size: var(--pico-font-size) !important; + line-height: var(--pico-line-height) !important; + transition: border-color var(--pico-transition), box-shadow var(--pico-transition) !important; + + &:focus { + @include focus-state; + } + + > input { + color: var(--pico-color) !important; + } +} + +// Tag styles +.selectize-input > .item { + display: inline-flex; + align-items: center; + background-color: rgba(map.get(colors.$palette, info), 0.1) !important; + color: map.get(colors.$palette, info) !important; + border-radius: 4px; + gap: 0.25rem; + font-size: map.get($tag-font-size, default); + font-weight: 500; + line-height: 1.125; + padding: map.get($tag-padding, default); + @include hover-transition; + + a { + color: var(--pico-primary) !important; + text-decoration: none; + + &:hover { + color: var(--pico-primary-hover) !important; + text-decoration: underline; + } + } + + &:hover { + background-color: rgba(map.get(colors.$palette, info), 0.15) !important; + } + + &.tag-large { + padding: map.get($tag-padding, large); + font-size: map.get($tag-font-size, large); + } + + &.tag-small { + padding: map.get($tag-padding, small); + font-size: map.get($tag-font-size, small); + } +} + +// Dropdown styles +.selectize-dropdown { + @include form-element-base; + box-shadow: var(--pico-card-box-shadow) !important; + margin-top: 0.25rem !important; + z-index: 50 !important; + padding: 0 !important; + + .selectize-dropdown-content { + max-height: 300px !important; + overflow-y: auto !important; + padding: 0.5rem 0 !important; + + .option { + padding: var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal) !important; + color: var(--pico-muted-color) !important; + cursor: pointer !important; + @include hover-transition; + border: none !important; + background: none !important; + + &:hover, &.active { + background-color: var(--pico-primary-hover) !important; + color: var(--pico-primary-inverse) !important; + } + + &.selected { + background-color: var(--pico-primary) !important; + color: var(--pico-primary-inverse) !important; + } + } + + .create { + padding: var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal) !important; + cursor: pointer !important; + @include hover-transition; + background-color: var(--pico-form-element-background-color) !important; + color: var(--pico-color) !important; + border: none !important; + } + } +} \ No newline at end of file diff --git a/app/frontend/styles/utils.scss b/app/frontend/styles/utils.scss new file mode 100644 index 0000000..2ed4c7e --- /dev/null +++ b/app/frontend/styles/utils.scss @@ -0,0 +1,50 @@ +.float-right { + float: right; +} + +.pointer { + cursor: pointer; +} + + +.admin-tool { + border: 1.5px dashed orange; + border-radius: 7px; + padding: 2px; +} + +.dev-tool { + border: 1.5px dashed lawngreen; + border-radius: 7px; + padding: 2px; +} + +.back-office-tool { + border: 1.7px dashed rgb(0, 0, 0); + padding: 2px; +} + +span.admin-tool { + border-radius: 8px; + padding: 3px 2px; +} + +.dev-border-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; + z-index: 9999; + border: 5px dashed #00FF00; + box-sizing: border-box; +} + +#confetti { + pointer-events: none; +} + +.w-100 { + width: 100%; +} \ No newline at end of file diff --git a/app/helpers/api/v1/application_helper.rb b/app/helpers/api/v1/application_helper.rb new file mode 100644 index 0000000..0cab3bb --- /dev/null +++ b/app/helpers/api/v1/application_helper.rb @@ -0,0 +1,28 @@ +module API + module V1 + module ApplicationHelper + def expand?(key) + @expand.include?(key) + end + + def if_expand(key, &block) + if expand?(key) + yield + end + end + + def expand(*keys) + before = @expand + @expand = @expand.dup + keys + + yield + ensure + @expand = before + end + + def pii(&block) + yield if @pii + end + end + end +end diff --git a/app/helpers/api/v1/shipments_helper.rb b/app/helpers/api/v1/shipments_helper.rb new file mode 100644 index 0000000..ee3ef65 --- /dev/null +++ b/app/helpers/api/v1/shipments_helper.rb @@ -0,0 +1,2 @@ +module API::V1::ShipmentsHelper +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 0000000..149bebf --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,101 @@ +module ApplicationHelper + include ButtonHelper + + def admin_tool(class_name: "", element: "div", **options, &block) + return unless current_user&.is_admin? + concat content_tag(element, class: "admin-tool #{class_name}", **options, &block) + end + + def dev_tool(class_name: "", element: "div", **options, &block) + return unless Rails.env.development? + concat content_tag(element, class: "dev-tool #{class_name}", **options, &block) + end + + def nav_item(path, text, options = {}) + content_tag("li") do + link_to path, class: current_page?(path) ? "active" : "", **options do + text + end + end + end + + def zenv_link(model) + return unless model.zenventory_url.present? + admin_tool element: :span do + link_to "Edit on Zenventory", model.zenventory_url, target: "_blank" + end + end + + def inspector_toggle(thing) + admin_tool(class_name: "mt4") do + param = "inspect_#{thing}".to_sym + if params[param] + link_to "uninspect #{thing}?", url_for(param => nil) + else + link_to "inspect #{thing}?", url_for(param => "yeah") + end + end + end + + def param_toggle(thing) + if params[thing] + link_to "hide #{thing}?", url_for(thing => nil) + else + link_to "show #{thing}?", url_for(thing => "yeah") + end + end + + def render_checkbox(value) + content_tag(:span, style: "color: var(--checkbox-#{value ? "true" : "false"})") { value ? "☑" : "☒" } + end + + def copy_to_clipboard(clipboard_value, tooltip_direction: "n", **options, &block) + # If block is not given, use clipboard_value as the rendered content + block ||= ->(_) { clipboard_value } + return yield if options.delete(:if) == false + + css_classes = "pointer tooltipped tooltipped--#{tooltip_direction} #{options.delete(:class)}" + tag.span "data-copy-to-clipboard": clipboard_value, class: css_classes, "aria-label": options.delete(:label) || "click to copy...", **options, &block + end + + def render_json_example(obj) + transformed = recursively_transform_values(obj) do |v| + "#{v}" + end + copy_to_clipboard(JSON.pretty_generate(obj)) do + content_tag("div") do + content_tag("pre") do + "
".html_safe + JSON.pretty_generate(transformed).html_safe + end + + content_tag("small") do + "(you can click that hunk of JSON to copy it if you like)" + end + end + end + end + + def available_tags(selected_tags = []) + Rails.cache.fetch("available_tags", expires_in: 1.day) do + common_tags = CommonTag.pluck(:tag) + warehouse_order_tags = Warehouse::Order.all_tags + letter_tags = Letter.all_tags + + (common_tags + (warehouse_order_tags + letter_tags).compact_blank.sort).uniq + end + end + + private + + def recursively_transform_values(obj, &block) + case obj + when Hash + obj.transform_values { |v| recursively_transform_values(v, &block) } + when Array + obj.map { |v| recursively_transform_values(v, &block) } + when String, Numeric, TrueClass, FalseClass, NilClass + block.call(obj) + else + obj + end + end +end diff --git a/app/helpers/batches_helper.rb b/app/helpers/batches_helper.rb new file mode 100644 index 0000000..5eb7140 --- /dev/null +++ b/app/helpers/batches_helper.rb @@ -0,0 +1,15 @@ +module BatchesHelper + def batch_status_badge(status, addtl_class='') + clazz, text = case status.to_s + when 'awaiting_field_mapping' + ['warning', 'awaiting field mapping'] + when 'fields_mapped' + ['info', 'ready to process'] + when 'processed' + ['success', 'processed'] + else + ['muted', status.to_s.humanize(capitalize: false)] + end + content_tag('span', text, class: "badge #{clazz} #{addtl_class}".strip) + end +end diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb new file mode 100644 index 0000000..8213b6b --- /dev/null +++ b/app/helpers/button_helper.rb @@ -0,0 +1,131 @@ +module ButtonHelper + def primary_button_to(name, url, options = {}) + options[:class] ||= "" + options[:class] += " btn btn-md btn-primary" + + button_to(name, url, options) + end + + def primary_link_to(name, url, options = {}, &block) + options[:class] ||= "" + options[:class] += " btn btn-md btn-primary" + + if block_given? + link_to(url, options, &block) + else + link_to(name, url, options) + end + end + + def primary_outline_link_to(name, url, options = {}, &block) + options[:class] ||= "" + options[:class] += " btn btn-small outlined" + + if block_given? + link_to(url, options, &block) + else + link_to(name, url, options) + end + end + + def secondary_link_to(name, url, options = {}, &block) + options[:class] ||= "" + options[:class] += " btn btn-sm btn-secondary" + + if block_given? + link_to(url, options, &block) + else + link_to(name, url, options) + end + end + + def danger_button_to(name, url, options = {}) + options[:class] ||= "" + options[:class] += " btn btn-md btn-danger" + + # Add confirmation by default for dangerous actions + options[:data] ||= {} + options[:data][:confirm] ||= "Are you sure? This action cannot be undone." + + button_to(name, url, options) + end + + def warning_link_to(name, url, options = {}, &block) + options[:class] ||= "" + options[:class] += " btn btn-md btn-warning" + + if block_given? + link_to(url, options, &block) + else + link_to(name, url, options) + end + end + + def success_link_to(name, url, options = {}, &block) + options[:class] ||= "" + options[:class] += " btn btn-md btn-success" + + if block_given? + link_to(url, options, &block) + else + link_to(name, url, options) + end + end + + # Common icons you can use with buttons + def back_icon + ' + + '.html_safe + end + + def edit_icon + ' + + '.html_safe + end + + def document_icon + ' + + '.html_safe + end + + def check_icon + ' + + '.html_safe + end + + def download_icon + ' + + '.html_safe + end + + def eye_icon + ' + + + '.html_safe + end + + def plus_icon + ' + + '.html_safe + end + + def create_button(url, text = "Create", options = {}, &block) + options[:class] ||= "" + options[:class] += " btn success" + + if block_given? + success_link_to(url, options) do + plus_icon + " #{text}" + end + else + success_link_to(plus_icon + " #{text}", url, options) + end + end +end diff --git a/app/helpers/customs_receipts_helper.rb b/app/helpers/customs_receipts_helper.rb new file mode 100644 index 0000000..7b5ff29 --- /dev/null +++ b/app/helpers/customs_receipts_helper.rb @@ -0,0 +1,2 @@ +module CustomsReceiptsHelper +end diff --git a/app/helpers/letter/queues_helper.rb b/app/helpers/letter/queues_helper.rb new file mode 100644 index 0000000..858070f --- /dev/null +++ b/app/helpers/letter/queues_helper.rb @@ -0,0 +1,2 @@ +module Letter::QueuesHelper +end diff --git a/app/helpers/letters_helper.rb b/app/helpers/letters_helper.rb new file mode 100644 index 0000000..72acbe0 --- /dev/null +++ b/app/helpers/letters_helper.rb @@ -0,0 +1,20 @@ +module LettersHelper + def letter_status_badge(status, addtl_class='nil') + + clazz, text = case status.to_s + when 'queued' + ['bg-muted', 'queued'] + when 'pending' + ['pending', 'ready to print'] + when "printed" + ["info", "printed"] + when "mailed" + ["success", "mailed"] + when "canceled", "failed" + "bg-error-bg text-error-fg border border-error-border" + else + "bg-smoke text-slate border border-muted" + end + content_tag('span', text, :class => "badge #{clazz} #{addtl_class}".strip) + end +end diff --git a/app/helpers/public/api/v1/application_helper.rb b/app/helpers/public/api/v1/application_helper.rb new file mode 100644 index 0000000..3547761 --- /dev/null +++ b/app/helpers/public/api/v1/application_helper.rb @@ -0,0 +1,26 @@ +module Public + module API + module V1 + module ApplicationHelper + def expand?(key) + @expand.include?(key) + end + + def if_expand(key, &block) + if expand?(key) + yield + end + end + + def expand(*keys) + before = @expand + @expand = @expand.dup + keys + + yield + ensure + @expand = before + end + end + end + end +end diff --git a/app/helpers/public/application_helper.rb b/app/helpers/public/application_helper.rb new file mode 100644 index 0000000..d942c14 --- /dev/null +++ b/app/helpers/public/application_helper.rb @@ -0,0 +1,10 @@ +module Public::ApplicationHelper + def w95_title_button_to(label, url) + content_tag("button", nil, {"aria-label"=>label, "onclick"=>%(window.location.href='#{url}';)}) + end + + def back_office_tool(class_name: "", element: "div", **options, &block) + return unless current_user + concat content_tag(element, class: "back-office-tool #{class_name}", **options, &block) + end +end \ No newline at end of file diff --git a/app/helpers/public/sessions_helper.rb b/app/helpers/public/sessions_helper.rb new file mode 100644 index 0000000..fd2be02 --- /dev/null +++ b/app/helpers/public/sessions_helper.rb @@ -0,0 +1,2 @@ +module Public::SessionsHelper +end diff --git a/app/helpers/public/static_pages_helper.rb b/app/helpers/public/static_pages_helper.rb new file mode 100644 index 0000000..b689328 --- /dev/null +++ b/app/helpers/public/static_pages_helper.rb @@ -0,0 +1,2 @@ +module Public::StaticPagesHelper +end diff --git a/app/helpers/source_tags_helper.rb b/app/helpers/source_tags_helper.rb new file mode 100644 index 0000000..c4c7f67 --- /dev/null +++ b/app/helpers/source_tags_helper.rb @@ -0,0 +1,2 @@ +module SourceTagsHelper +end diff --git a/app/helpers/static_pages_helper.rb b/app/helpers/static_pages_helper.rb new file mode 100644 index 0000000..2d63e79 --- /dev/null +++ b/app/helpers/static_pages_helper.rb @@ -0,0 +1,2 @@ +module StaticPagesHelper +end diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb new file mode 100644 index 0000000..23450bc --- /dev/null +++ b/app/helpers/tags_helper.rb @@ -0,0 +1,2 @@ +module TagsHelper +end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb new file mode 100644 index 0000000..2310a24 --- /dev/null +++ b/app/helpers/users_helper.rb @@ -0,0 +1,2 @@ +module UsersHelper +end diff --git a/app/helpers/usps/indicia_helper.rb b/app/helpers/usps/indicia_helper.rb new file mode 100644 index 0000000..b058157 --- /dev/null +++ b/app/helpers/usps/indicia_helper.rb @@ -0,0 +1,2 @@ +module USPS::IndiciaHelper +end diff --git a/app/helpers/usps/iv_mtr/webhook_helper.rb b/app/helpers/usps/iv_mtr/webhook_helper.rb new file mode 100644 index 0000000..723c82f --- /dev/null +++ b/app/helpers/usps/iv_mtr/webhook_helper.rb @@ -0,0 +1,2 @@ +module USPS::IVMTR::WebhookHelper +end diff --git a/app/helpers/usps/mailer_ids_helper.rb b/app/helpers/usps/mailer_ids_helper.rb new file mode 100644 index 0000000..65e516c --- /dev/null +++ b/app/helpers/usps/mailer_ids_helper.rb @@ -0,0 +1,2 @@ +module USPS::MailerIdsHelper +end diff --git a/app/helpers/usps/oauth2_connections_helper.rb b/app/helpers/usps/oauth2_connections_helper.rb new file mode 100644 index 0000000..95b23b6 --- /dev/null +++ b/app/helpers/usps/oauth2_connections_helper.rb @@ -0,0 +1,2 @@ +module USPS::Oauth2ConnectionsHelper +end diff --git a/app/helpers/usps/payment_accounts_helper.rb b/app/helpers/usps/payment_accounts_helper.rb new file mode 100644 index 0000000..36e39de --- /dev/null +++ b/app/helpers/usps/payment_accounts_helper.rb @@ -0,0 +1,2 @@ +module USPS::PaymentAccountsHelper +end diff --git a/app/helpers/warehouse/orders_helper.rb b/app/helpers/warehouse/orders_helper.rb new file mode 100644 index 0000000..ebc1832 --- /dev/null +++ b/app/helpers/warehouse/orders_helper.rb @@ -0,0 +1,2 @@ +module Warehouse::OrdersHelper +end diff --git a/app/helpers/warehouse/purpose_codes_helper.rb b/app/helpers/warehouse/purpose_codes_helper.rb new file mode 100644 index 0000000..2673c03 --- /dev/null +++ b/app/helpers/warehouse/purpose_codes_helper.rb @@ -0,0 +1,2 @@ +module Warehouse::PurposeCodesHelper +end diff --git a/app/helpers/warehouse/skus_helper.rb b/app/helpers/warehouse/skus_helper.rb new file mode 100644 index 0000000..435e904 --- /dev/null +++ b/app/helpers/warehouse/skus_helper.rb @@ -0,0 +1,2 @@ +module Warehouse::SKUsHelper +end diff --git a/app/helpers/warehouse/templates_helper.rb b/app/helpers/warehouse/templates_helper.rb new file mode 100644 index 0000000..8da70e5 --- /dev/null +++ b/app/helpers/warehouse/templates_helper.rb @@ -0,0 +1,2 @@ +module Warehouse::TemplatesHelper +end diff --git a/app/javascript/application.js b/app/javascript/application.js new file mode 100644 index 0000000..ec03a55 --- /dev/null +++ b/app/javascript/application.js @@ -0,0 +1,25 @@ +import "./controllers" +import "jquery" +import "@nathanvda/cocoon"; +import "select2" + +import "@selectize/selectize"; + +// $(document).ready (()=> { +// $( ".needs-select2" ).select2() +// document.addEventListener('vanilla-nested:fields-added', (e)=>{ +// $(e.target).find('.needs-select2').select2(); +// }); +// }); + +// (select_the_2) +// $(document).on('vanilla-nested:fields-added')(select_the_2) + +// Add development border overlay in non-production environments +document.addEventListener('DOMContentLoaded', () => { + if (document.body.classList.contains('not_prod')) { + const devBorderOverlay = document.createElement('div'); + devBorderOverlay.className = 'dev-border-overlay'; + document.body.appendChild(devBorderOverlay); + } +}); diff --git a/app/javascript/controllers/application.js b/app/javascript/controllers/application.js new file mode 100644 index 0000000..1213e85 --- /dev/null +++ b/app/javascript/controllers/application.js @@ -0,0 +1,9 @@ +import { Application } from "@hotwired/stimulus" + +const application = Application.start() + +// Configure Stimulus development experience +application.debug = false +window.Stimulus = application + +export { application } diff --git a/app/javascript/controllers/batch_type_controller.js b/app/javascript/controllers/batch_type_controller.js new file mode 100644 index 0000000..5069ff6 --- /dev/null +++ b/app/javascript/controllers/batch_type_controller.js @@ -0,0 +1,21 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["warehouseTemplate"] + + connect() { + this.toggleWarehouseTemplate() + } + + toggleWarehouseTemplate() { + const typeSelect = this.element.querySelector('select[name="batch[type]"]') + const selectedType = typeSelect.value + const isWarehouse = selectedType === 'Warehouse::Batch' + + this.warehouseTemplateTarget.style.display = isWarehouse ? 'block' : 'none' + } + + change() { + this.toggleWarehouseTemplate() + } +} \ No newline at end of file diff --git a/app/javascript/controllers/hello_controller.js b/app/javascript/controllers/hello_controller.js new file mode 100644 index 0000000..5975c07 --- /dev/null +++ b/app/javascript/controllers/hello_controller.js @@ -0,0 +1,7 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + connect() { + this.element.textContent = "Hello World!" + } +} diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js new file mode 100644 index 0000000..d0685d3 --- /dev/null +++ b/app/javascript/controllers/index.js @@ -0,0 +1,8 @@ +// This file is auto-generated by ./bin/rails stimulus:manifest:update +// Run that command whenever you add a new controller or create them with +// ./bin/rails generate stimulus controllerName + +import { application } from "./application" + +import HelloController from "./hello_controller" +application.register("hello", HelloController) diff --git a/app/javascript/controllers/queue_type_controller.js b/app/javascript/controllers/queue_type_controller.js new file mode 100644 index 0000000..326c130 --- /dev/null +++ b/app/javascript/controllers/queue_type_controller.js @@ -0,0 +1,19 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["instantFields", "paymentAccountField"] + + connect() { + this.toggleFields() + } + + toggleFields() { + const type = this.element.querySelector('select[name="letter_queue[type]"]').value + this.instantFieldsTarget.style.display = type === "Letter::InstantQueue" ? "block" : "none" + } + + togglePaymentAccount(event) { + const postageType = event.target.value + this.paymentAccountFieldTarget.style.display = postageType === "indicia" ? "block" : "none" + } +} \ No newline at end of file diff --git a/app/javascript/entrypoints/application.js b/app/javascript/entrypoints/application.js new file mode 100644 index 0000000..b679e2a --- /dev/null +++ b/app/javascript/entrypoints/application.js @@ -0,0 +1,28 @@ +// To see this message, add the following to the `` section in your +// views/layouts/application.html.erb +// +// <%= vite_client_tag %> +// <%= vite_javascript_tag 'application' %> +console.log('Vite ⚡️ Rails') + +// If using a TypeScript entrypoint file: +// <%= vite_typescript_tag 'application' %> +// +// If you want to use .jsx or .tsx, add the extension: +// <%= vite_javascript_tag 'application.jsx' %> + +console.log('Visit the guide for more information: ', 'https://vite-ruby.netlify.app/guide/rails') + +// Example: Load Rails libraries in Vite. +// +// import * as Turbo from '@hotwired/turbo' +// Turbo.start() +// +// import ActiveStorage from '@rails/activestorage' +// ActiveStorage.start() +// +// // Import all channels. +// const channels = import.meta.globEager('./**/*_channel.js') + +// Example: Import a stylesheet in app/frontend/index.css +// import '~/index.css' diff --git a/app/javascript/turbo.js b/app/javascript/turbo.js new file mode 100644 index 0000000..48f299d --- /dev/null +++ b/app/javascript/turbo.js @@ -0,0 +1,8 @@ +// import { Turbo } from "@hotwired/turbo-rails" + +// // Disable Turbo for all form submissions by default +// document.addEventListener("turbo:load", () => { +// document.querySelectorAll("form").forEach(form => { +// form.dataset.turbo = "false" +// }) +// }) \ No newline at end of file diff --git a/app/jobs/airtable_etl/airtable_etl_job.rb b/app/jobs/airtable_etl/airtable_etl_job.rb new file mode 100644 index 0000000..fe86cd3 --- /dev/null +++ b/app/jobs/airtable_etl/airtable_etl_job.rb @@ -0,0 +1,59 @@ +module AirtableETL + class AirtableETLJob < ApplicationJob + def perform + type = self.class::TYPE + raise "type must be one of: :letters, :warehouse_orders" unless [:letters, :warehouse_orders].include?(type) + base_key = self.class::BASE_KEY + table_name = self.class::TABLE_NAME + field_map = self.class::FIELD_MAP + + table = Class.new(Norairrecord::Table) do + self.base_key = base_key + self.table_name = table_name + end + + recs = table.records + + case type + when :letters + recs.each do |rec| + public_id = rec[field_map[:public_id]] + + letter = Letter.find_by_public_id(public_id) + if letter.nil? + Rails.logger.error("Letter not found for public_id: #{public_id}") + rec[field_map[:aasm_state]] = "not_found" + next + end + + field_map.each do |theseus_field, airtable_field| + res = letter.send(theseus_field) + if res.is_a?(BigDecimal) + res = res.to_f + end + + if res.is_a?(Time) && rec[airtable_field].is_a?(String) + begin + airtable_time = Time.parse(rec[airtable_field]) + next if res.to_i == airtable_time.to_i + rescue ArgumentError + end + end + + next if res == rec[airtable_field] + puts "Updating #{airtable_field} for #{public_id} from #{rec[airtable_field]} to #{res}" + rec[airtable_field] = res + end + end + when :warehouse_orders + raise NotImplementedError + end + + recs.reject! do |rec| + rec.instance_variable_get(:@updated_keys).empty? + end + + table.batch_update(recs) + end + end +end diff --git a/app/jobs/airtable_etl/athena_stickers_etl_job.rb b/app/jobs/airtable_etl/athena_stickers_etl_job.rb new file mode 100644 index 0000000..7dead3d --- /dev/null +++ b/app/jobs/airtable_etl/athena_stickers_etl_job.rb @@ -0,0 +1,16 @@ +module AirtableETL + class AthenaStickersETLJob < AirtableETLJob + TYPE = :letters + FIELD_MAP = { + public_id: "Theseus – Letter ID", + aasm_state: "Theseus – Status", + mailed_at: "Theseus – Mailed At", + received_at: "Theseus – Received At", + postage: "Theseus – Postage Cost", + } + BASE_KEY = "appwSxpT4lASsosUI" + TABLE_NAME = "tblBOgmyC9RVK98wD" + end +end + +# https://airtable.com/appwSxpT4lASsosUI/tblBOgmyC9RVK98wD/viw8A7jEmV36w3zAe/recM62E73UpmG0W1C/fld65tjhaen9iv77b?copyLinkToCellOrRecordOrigin=gridView diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 0000000..d394c3d --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/app/jobs/customs_receipt/msr_receipt_job.rb b/app/jobs/customs_receipt/msr_receipt_job.rb new file mode 100644 index 0000000..7ebb59a --- /dev/null +++ b/app/jobs/customs_receipt/msr_receipt_job.rb @@ -0,0 +1,17 @@ +class CustomsReceipt::MSRReceiptJob < ApplicationJob + queue_as :default + + def perform(msr_id) + msr = LSV::MarketingShipmentRequest.find(msr_id) + + receiptable = CustomsReceipt::TheseusSpecific.receiptable_from_msr(msr) + pdf = CustomsReceipt::Generate.run(receiptable) + + # Send the email with PDF attachment + CustomsReceipt::ReceiptMailer.with( + email: msr.email, + order_number: msr.id, + pdf_data: pdf, + ).receipt.deliver_now + end +end diff --git a/app/jobs/customs_receipt/warehouse_order_receipt_job.rb b/app/jobs/customs_receipt/warehouse_order_receipt_job.rb new file mode 100644 index 0000000..316cae1 --- /dev/null +++ b/app/jobs/customs_receipt/warehouse_order_receipt_job.rb @@ -0,0 +1,17 @@ +class CustomsReceipt::WarehouseOrderReceiptJob < ApplicationJob + queue_as :default + + def perform(warehouse_order_id) + order = Warehouse::Order.find(warehouse_order_id) + + receiptable = CustomsReceipt::TheseusSpecific.receiptable_from_warehouse_order(order) + pdf = CustomsReceipt::Generate.run(receiptable) + + # Send the email with PDF attachment + CustomsReceipt::ReceiptMailer.with( + email: order.recipient_email, + order_number: order.hc_id, + pdf_data: pdf, + ).receipt.deliver_now + end +end diff --git a/app/jobs/one_time/backfill_warehouse_postage_costs_job.rb b/app/jobs/one_time/backfill_warehouse_postage_costs_job.rb new file mode 100644 index 0000000..35f2d28 --- /dev/null +++ b/app/jobs/one_time/backfill_warehouse_postage_costs_job.rb @@ -0,0 +1,29 @@ +class OneTime::BackfillWarehousePostageCostsJob < ApplicationJob + queue_as :default + + FUDGE_FACTOR = 4.weeks + + def perform(*args) + orders = Warehouse::Order.mailed.order(mailed_at: :asc) + + return if orders.empty? + + start_date = orders.first.dispatched_at - FUDGE_FACTOR + end_date = orders.last.dispatched_at + FUDGE_FACTOR + + zen_orders = Zenventory.run_report( + "shipment", + "ship_client", + startDate: start_date, + endDate: end_date + ).index_by { |order| order[:order_number].sub("hack.club/", "") } + + orders.each do |order| + zen_order = zen_orders[order.hc_id] + next unless zen_order + order.update( + postage_cost: zen_order[:shipping_handling], + ) + end + end +end diff --git a/app/jobs/one_time/import_skus_from_airtable_job.rb b/app/jobs/one_time/import_skus_from_airtable_job.rb new file mode 100644 index 0000000..7ff6675 --- /dev/null +++ b/app/jobs/one_time/import_skus_from_airtable_job.rb @@ -0,0 +1,41 @@ +class OneTime::ImportSKUsFromAirtableJob < ApplicationJob + queue_as :default + + def perform(*args) + at_skus = Airtable::WarehouseSKU.where("NOT({Item Type}='Kit')") + + at_skus.each do |at_sku| + Warehouse::SKU.find_or_create_by!(sku: at_sku.sku) do |sku| + sku.name = at_sku.name + sku.category = case at_sku.item_type + when "Printed Material" + name = at_sku.name.downcase + if name.include? "card" + :card + elsif name.include? "poster" + :poster + elsif name.include? "flyer" + :flyer + else + :other_printed_material + end + when "Grant" + :grant + when "Sticker" + :sticker + when "Hardware" + :hardware + when "Swag" + :swag + when "Book" + :book + when "Prize" + :prize + end + sku.in_stock = at_sku.in_stock + sku.average_po_cost = at_sku.unit_cost + sku.actual_cost_to_hc = at_sku.unit_cost + end + end + end +end diff --git a/app/jobs/public/send_login_email_job.rb b/app/jobs/public/send_login_email_job.rb new file mode 100644 index 0000000..75dbffe --- /dev/null +++ b/app/jobs/public/send_login_email_job.rb @@ -0,0 +1,10 @@ +module Public + class SendLoginEmailJob < ApplicationJob + queue_as :default + + def perform(email) + code = Public::User.find_or_create_by!(email:).create_login_code + LoginCodeMailer.send_login_code(email, code).deliver_later + end + end +end \ No newline at end of file diff --git a/app/jobs/public/update_map_data_job.rb b/app/jobs/public/update_map_data_job.rb new file mode 100644 index 0000000..d9d22d6 --- /dev/null +++ b/app/jobs/public/update_map_data_job.rb @@ -0,0 +1,117 @@ +class Public::UpdateMapDataJob < ApplicationJob + queue_as :default + + def perform + map_data = fetch_recent_letters_data + Rails.cache.write("map_data", map_data, expires_in: 1.hour) + map_data + end + + private + + def fetch_recent_letters_data + # Get mailed letters (any time) and letters received in the last 7 days + recent_letters = Letter.joins(:address, :return_address) + .where( + "aasm_state = 'mailed' OR (aasm_state = 'received' AND received_at >= ?)", + 7.days.ago + ) + .includes(:iv_mtr_events, :address, :return_address) + + letters_data = recent_letters.map do |letter| + event_coords = build_letter_event_coordinates(letter) + + bubble_title = if letter.aasm_state == "received" + "a letter was received here!" + elsif letter.iv_mtr_events.blank? + "a letter was mailed here!" + else + "a letter was last seen here!" + end + + { + coordinates: event_coords, + current_location: event_coords.last, + destination_coords: geocode_destination(letter.address), + bubble_title: bubble_title, + aasm_state: letter.aasm_state, + } + end + + letters_data + end + + def build_letter_event_coordinates(letter) + coordinates = [] + + # Mailed event coordinates + if letter.mailed_at.present? + coords = geocode_origin(letter.return_address) + coordinates << coords + end + + # USPS tracking event coordinates (ordered by scan datetime) + if letter.iv_mtr_events.present? + # Order events by scan datetime to get proper chronological order + ordered_events = letter.iv_mtr_events.sort_by { |event| event.scan_datetime || event.happened_at } + + ordered_events.each do |event| + begin + hydrated = event.hydrated + locale_key = hydrated.scan_locale_key + if locale_key.present? + coords = geocode_usps_facility(locale_key) + coordinates << coords if coords + end + rescue => e + Rails.logger.warn("Failed to process IV-MTR event #{event.id}: #{e.message}") + # Continue processing other events + end + end + end + + # Received event coordinates + if letter.received_at.present? + coords = geocode_destination(letter.address) + coordinates << coords + end + + coordinates.compact + end + + def geocode_origin(return_address) + # Special case: anything in Shelburne goes to FIFTEEN_FALLS + if return_address.city&.downcase&.include?("shelburne") + return { + lat: GeocodingService::FIFTEEN_FALLS[:lat].to_f, + lon: GeocodingService::FIFTEEN_FALLS[:lon].to_f, + } + end + + # Use non-exact geocoding to avoid doxing + result = GeocodingService.geocode_return_address(return_address, exact: false) + { + lat: result[:lat].to_f, + lon: result[:lon].to_f, + } + end + + def geocode_destination(address) + # Use non-exact geocoding (city only) to avoid doxing + result = GeocodingService.geocode_address_model(address, exact: false) + { + lat: result[:lat].to_f, + lon: result[:lon].to_f, + } + end + + def geocode_usps_facility(locale_key) + result = GeocodingService::USPSFacilities.coords_for_locale_key(locale_key) + return nil unless result + + { + lat: result[:lat].to_f, + lon: result[:lon].to_f, + } + end +end diff --git a/app/jobs/table_sync/order_sync_job.rb b/app/jobs/table_sync/order_sync_job.rb new file mode 100644 index 0000000..6401749 --- /dev/null +++ b/app/jobs/table_sync/order_sync_job.rb @@ -0,0 +1,7 @@ +class TableSync::OrderSyncJob < TableSync::TableSyncJob + def perform(*args) + @model = Warehouse::Order + @sync_id = ENV["AIRTABLE_ORDER_SYNC_ID"] + super + end +end diff --git a/app/jobs/table_sync/sku_sync_job.rb b/app/jobs/table_sync/sku_sync_job.rb new file mode 100644 index 0000000..e9b51fe --- /dev/null +++ b/app/jobs/table_sync/sku_sync_job.rb @@ -0,0 +1,7 @@ +class TableSync::SKUSyncJob < TableSync::TableSyncJob + def perform(*args) + @model = Warehouse::SKU + @sync_id = ENV["AIRTABLE_SKU_SYNC_ID"] + super + end +end diff --git a/app/jobs/table_sync/table_sync_job.rb b/app/jobs/table_sync/table_sync_job.rb new file mode 100644 index 0000000..8f5d246 --- /dev/null +++ b/app/jobs/table_sync/table_sync_job.rb @@ -0,0 +1,10 @@ +class TableSync::TableSyncJob < ApplicationJob + queue_as :default + def perform(*args) + unless @sync_id + Rails.logger.warn("no sync id for #{self.class.name}!") + return + end + @model&.mirror_to_airtable!(@sync_id) + end +end diff --git a/app/jobs/update_tag_cache_job.rb b/app/jobs/update_tag_cache_job.rb new file mode 100644 index 0000000..29626dc --- /dev/null +++ b/app/jobs/update_tag_cache_job.rb @@ -0,0 +1,8 @@ +class UpdateTagCacheJob < ApplicationJob + queue_as :default + + def perform + Rails.cache.delete("available_tags") + ApplicationController.helpers.available_tags + end +end \ No newline at end of file diff --git a/app/jobs/user/update_tasks_job.rb b/app/jobs/user/update_tasks_job.rb new file mode 100644 index 0000000..5754a48 --- /dev/null +++ b/app/jobs/user/update_tasks_job.rb @@ -0,0 +1,61 @@ +class User + class UpdateTasksJob < ApplicationJob + queue_as :default + + def perform(user) + tasks = gather_tasks(user) + Rails.cache.write("user_tasks/#{user.id}", tasks, expires_in: 5.minutes) + tasks + end + + private + + def gather_tasks(user) + queues = user.letter_queues + .select("letter_queues.name, letter_queues.slug, COUNT(letters.id) as letter_count") + .joins(:letters) + .where(letters: { aasm_state: "queued" }) + .group("letter_queues.name, letter_queues.slug") + .map do |queue| + { + type: "queues with waiting letters", + name: queue.name, + subtitle: "#{queue.letter_count} #{"letter".pluralize(queue.letter_count)} queued...", + count: queue.letter_count, + link: Rails.application.routes.url_helpers.letter_queue_path(queue.slug, anchor: "letters"), + } + end + + batches = user.batches + .where(aasm_state: "fields_mapped").map do |batch| + { + type: "Batches awaiting processing", + name: "#{batch.class.name.split("::").first} batch ##{batch.id}", + subtitle: "#{batch.origin}#{batch.tags.any? ? " [#{batch.tags.join(", ")}]" : nil}", + link: case batch + when Warehouse::Batch + Rails.application.routes.url_helpers.process_warehouse_batch_path(batch) + when Letter::Batch + Rails.application.routes.url_helpers.process_letter_batch_path(batch) + else + Rails.application.routes.url_helpers.process_batch_path(batch) + end, + } + end + + letters = user.letters + .printed + .includes(:address) + .map do |letter| + { + type: "Letters printed but not marked mailed", + name: "Letter #{letter.public_id} – #{letter.user_facing_title || letter.tags.join(", ")}", + subtitle: "to #{letter.address.name_line}", + link: Rails.application.routes.url_helpers.letter_path(letter), + } + end + + queues + batches + letters + end + end +end diff --git a/app/jobs/usps/iv_mtr/import_events_job.rb b/app/jobs/usps/iv_mtr/import_events_job.rb new file mode 100644 index 0000000..73c6f73 --- /dev/null +++ b/app/jobs/usps/iv_mtr/import_events_job.rb @@ -0,0 +1,29 @@ +class USPS::IVMTR::ImportEventsJob < ApplicationJob + queue_as :default + + MAPPING = { + "scanDatetime" => :happened_at, + "scanEventCode" => :opcode, + "scanFacilityZip" => :zip_code + } + + def perform(batch) + ActiveRecord::Base.transaction do + batch.payload.each do |event| + # Extract the mailer ID from the IMB + imb_mid = event["imbMid"] + mailer_id = USPS::MailerId.find_by(mid: imb_mid) + + # Find or create the event + USPS::IVMTR::Event.find_or_create_from_payload( + event, + batch.id, + mailer_id.id + ) + end + + # Mark the batch as processed + batch.update!(processed: true) + end + end +end diff --git a/app/jobs/usps/iv_mtr/notify_slack_job.rb b/app/jobs/usps/iv_mtr/notify_slack_job.rb new file mode 100644 index 0000000..6672dae --- /dev/null +++ b/app/jobs/usps/iv_mtr/notify_slack_job.rb @@ -0,0 +1,67 @@ +class USPS::IVMTR::NotifySlackJob < ApplicationJob + queue_as :default + + def perform(event) + return unless event + + message = format_slack_message(event) + post_to_slack(message) + end + + private + + def format_slack_message(event) + + e = event.hydrated + + [ + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": if event.bogon? + ":neocat_laptop_notice: hey <@U06QK6AG3RD>! we got a bogon! #{event.imb_serial_number} on #{event.mailer_id.id} @ ??" + else + ":mailbox_with_mail: new IV event for *Letter #{event.letter.imb_serial_number}/#{event.letter.imb_rollover_count} on #{event.letter.usps_mailer_id.id}* @ !" + end + } + ] + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "[OP#{e.opcode.code}] *#{e.opcode.process_description}*\n_#{e.handling_event_type_description} – #{e.mail_phase} – #{e.machine_name} (#{event.payload.dig("machineId") || "no ID"})_" + } + ] + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "in #{e.scan_facility_name} (#{e.scan_locale_key}) @ #{e.scan_facility_city}, #{e.scan_facility_state} #{e.scan_facility_zip}" + } + ] + }, + { + "type": "context", + "elements": [ + { + "type": "plain_text", + "text": "#{event.hydrated.opcode.machine_type} / #{event.hydrated.opcode.equipment_description} (iid #{event.public_id})", + } + ] + } + ] + + end + + def post_to_slack(message) + Slack::Notifier.new ENV.fetch("IV_MTR_WEBHOOK_URL") do + defaults channel: "#ivmtr-feed", username: "iv-mtr" + end.ping blocks: message + end +end \ No newline at end of file diff --git a/app/jobs/usps/payment_account/pocket_watch_job.rb b/app/jobs/usps/payment_account/pocket_watch_job.rb new file mode 100644 index 0000000..7710c0c --- /dev/null +++ b/app/jobs/usps/payment_account/pocket_watch_job.rb @@ -0,0 +1,19 @@ +class USPS::PaymentAccount::PocketWatchJob < ApplicationJob + THRESHOLD = 50 # 50 bucks seems reasonable? + + queue_as :default + + def perform(*args) + broke_accounts = [] + + USPS::PaymentAccount.all.each do |acct| + if acct.ach? + Rails.logger.info("skipping pacc #{acct.id} because it's ach") + next + end + broke_accounts << acct unless acct.check_funds_available(THRESHOLD) + end + + USPS::PaymentAccountMailer.get_your_eps_racks_up(accounts: broke_accounts).deliver_later if broke_accounts.any? + end +end diff --git a/app/jobs/warehouse/update_cancellations_job.rb b/app/jobs/warehouse/update_cancellations_job.rb new file mode 100644 index 0000000..91257a2 --- /dev/null +++ b/app/jobs/warehouse/update_cancellations_job.rb @@ -0,0 +1,10 @@ +class Warehouse::UpdateCancellationsJob < ApplicationJob + queue_as :default + + def perform(*args) + canceled_zenventory_orders = Zenventory + .get_customer_orders(cancelled: true) + .map { |order| order[:orderNumber].sub("hack.club/", "") } + Warehouse::Order.where(hc_id: canceled_zenventory_orders).update_all(aasm_state: "canceled") + end +end diff --git a/app/jobs/warehouse/update_inventory_levels_job.rb b/app/jobs/warehouse/update_inventory_levels_job.rb new file mode 100644 index 0000000..b3f8d81 --- /dev/null +++ b/app/jobs/warehouse/update_inventory_levels_job.rb @@ -0,0 +1,44 @@ +class Warehouse::UpdateInventoryLevelsJob < ApplicationJob + queue_as :default + + def perform(*args) + Rails.logger.info("haiii!! it's ya girl cronjob coming to you with a hot new inventory update!") + Rails.logger.info("taking zenventory...") + inventory = Zenventory + .get_inventory + .index_by { |i| i.dig(:item, :sku) } + + Rails.logger.info("achievement get! fetched #{inventory.length} inventory items ^_^") + + Rails.logger.info("crunching unit cost numbers...") + unit_costs = Zenventory.get_purchase_orders + .flat_map { |po| po[:items] } + .group_by { |item| item[:sku] } + .transform_values do |items| + total_quantity = items.sum { |item| item[:quantity] } + total_cost = items.sum { |item| item[:quantity] * item[:unitCost] } + total_quantity.zero? ? 0 : total_cost.to_f / total_quantity + end + Rails.logger.info("okay!") + + Warehouse::SKU.all.each do |i| + sku = i.sku + inv_item = inventory[sku] + unit_cost = unit_costs[sku] + unless inv_item + Rails.logger.error("no item for #{sku} in warehouse inventory!") + next + end + i.update( + inbound: nilify(inv_item[:inbound]), + average_po_cost: unit_cost&.round(4), + in_stock: nilify(inv_item[:sellable]), + zenventory_id: inv_item.dig(:item, :id) + ) + end + end + + def nilify(val) + val&.zero? ? nil : val + end +end diff --git a/app/jobs/warehouse/update_mailing_info_job.rb b/app/jobs/warehouse/update_mailing_info_job.rb new file mode 100644 index 0000000..39a25ea --- /dev/null +++ b/app/jobs/warehouse/update_mailing_info_job.rb @@ -0,0 +1,36 @@ +class Warehouse::UpdateMailingInfoJob < ApplicationJob + queue_as :default + + FUDGE_FACTOR = 2.weeks + + def perform(*args) + orders = Warehouse::Order.dispatched.order(dispatched_at: :asc) + + return if orders.empty? + + start_date = orders.first.dispatched_at - FUDGE_FACTOR + end_date = orders.last.dispatched_at + FUDGE_FACTOR + + zen_orders = Zenventory.run_report( + "shipment", + "ship_client", + startDate: start_date, + endDate: end_date + ).index_by { |order| order[:order_number].sub("hack.club/", "") } + + orders.each do |order| + zen_order = zen_orders[order.hc_id] + next unless zen_order + order.update( + carrier: zen_order[:carrier], + service: zen_order[:service], + weight: zen_order[:weight], + tracking_number: zen_order[:tracking_number], + mailed_at: DateTime.parse(zen_order[:shipped_date]), + postage_cost: zen_order[:shipping_handling], + aasm_state: "mailed" + ) + Warehouse::OrderMailer.with(order:).order_shipped.deliver_later + end + end +end diff --git a/app/jobs/warehouse/update_median_postage_costs_job.rb b/app/jobs/warehouse/update_median_postage_costs_job.rb new file mode 100644 index 0000000..8e8ed4c --- /dev/null +++ b/app/jobs/warehouse/update_median_postage_costs_job.rb @@ -0,0 +1,6 @@ +class Warehouse::UpdateMedianPostageCostsJob < ApplicationJob + queue_as :default + + def perform(*args) + end +end diff --git a/app/lib/external_token.rb b/app/lib/external_token.rb new file mode 100644 index 0000000..4275c0b --- /dev/null +++ b/app/lib/external_token.rb @@ -0,0 +1,11 @@ +class ExternalToken < SimpleStructuredSecrets + def initialize(type) + @org = "th" + @type = type + end + + def generate + random = base62_encode(SecureRandom.rand(10 ** 60)).to_s[0...30] + "#{@org}_#{@type}_#{Rails.env.production? ? "live" : "dev"}_#{random}#{calc_checksum(random)}" + end +end \ No newline at end of file diff --git a/app/lib/frickin_country_names.rb b/app/lib/frickin_country_names.rb new file mode 100644 index 0000000..c8e8c59 --- /dev/null +++ b/app/lib/frickin_country_names.rb @@ -0,0 +1,61 @@ +# this is written in blood. + +module FrickinCountryNames + class << self + # all of these have been problems: + SILLY_LOOKUP_TABLE = { + "hong kong sar" => "HK", + "россия" => "RU", + } + UNSTATABLE_COUNTRIES = %w[EG SG] + + def find_country(string_to_ponder) + normalized = ActiveSupport::Inflector.transliterate(string_to_ponder.strip) + country = ISO3166::Country.find_country_by_alpha2(normalized) || + ISO3166::Country.find_country_by_alpha3(normalized) || + ISO3166::Country.find_country_by_any_name(normalized) || + ISO3166::Country.find_country_by_alpha2(SILLY_LOOKUP_TABLE[string_to_ponder.strip.downcase]) || + ISO3166::Country.find_country_by_alpha2(SILLY_LOOKUP_TABLE[normalized.downcase]) + end + + def find_country!(string_to_ponder) + country = find_country(string_to_ponder) + raise ArgumentError, "couldn't parse #{string_to_ponder} as a country!" unless country + country + end + + def find_state(country, string_to_ponder) + country&.find_subdivision_by_any_name(string_to_ponder) + end + + def find_state!(country, string_to_ponder) + state = find_state(country, string_to_ponder) + raise ArgumentError, "couldn't parse #{string_to_ponder} as a state!" unless state + state + end + + def normalize_state(country, string_to_ponder) + return string_to_ponder if UNSTATABLE_COUNTRIES.include?(country.alpha2) + find_state(country, string_to_ponder)&.code || string_to_ponder + end + end +end + +# lol countries can't find subdivisions by unofficial names +module ISO3166 + module CountrySubdivisionMethods + def find_subdivision_by_any_name(subdivision_str) + subdivisions.select do |k, v| + subdivision_str == k || v.name == subdivision_str || v.translations&.values.include?(subdivision_str) || v.unofficial_names&.include?(subdivision_str) || stupid_compare(v.translations&.values, subdivision_str) || v.unofficial_names && stupid_compare(Array(v.unofficial_names), subdivision_str) + end.values.first + end + + def stupid_compare(arr, val) + arr.map { |s| tldc(s) }.include?(val) + end + + def tldc(s) + ActiveSupport::Inflector.transliterate(s.strip).downcase + end + end +end diff --git a/app/lib/snail_but_nbsp.rb b/app/lib/snail_but_nbsp.rb new file mode 100644 index 0000000..a95cfb4 --- /dev/null +++ b/app/lib/snail_but_nbsp.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# just like good ol' Snail except s/ /nbsp and s/-/endash on city line +# +# that way we don't linebreak in the middle of a zipcode when we render out in Prawn! +class SnailButNbsp < Snail + def city_line + super.tr(" -", " –") + end +end diff --git a/app/lib/snail_mail/assets/fonts/arial.otf b/app/lib/snail_mail/assets/fonts/arial.otf new file mode 100644 index 0000000..494b56c Binary files /dev/null and b/app/lib/snail_mail/assets/fonts/arial.otf differ diff --git a/app/lib/snail_mail/assets/fonts/comic sans.ttf b/app/lib/snail_mail/assets/fonts/comic sans.ttf new file mode 100644 index 0000000..831e3d8 Binary files /dev/null and b/app/lib/snail_mail/assets/fonts/comic sans.ttf differ diff --git a/app/lib/snail_mail/assets/fonts/f25.ttf b/app/lib/snail_mail/assets/fonts/f25.ttf new file mode 100644 index 0000000..59d39cd Binary files /dev/null and b/app/lib/snail_mail/assets/fonts/f25.ttf differ diff --git a/app/lib/snail_mail/assets/fonts/gohu.ttf b/app/lib/snail_mail/assets/fonts/gohu.ttf new file mode 100644 index 0000000..46d79a0 Binary files /dev/null and b/app/lib/snail_mail/assets/fonts/gohu.ttf differ diff --git a/app/lib/snail_mail/assets/fonts/imb.ttf b/app/lib/snail_mail/assets/fonts/imb.ttf new file mode 100755 index 0000000..4b7131e Binary files /dev/null and b/app/lib/snail_mail/assets/fonts/imb.ttf differ diff --git a/app/lib/snail_mail/assets/fonts/noto sans regular.ttf b/app/lib/snail_mail/assets/fonts/noto sans regular.ttf new file mode 100755 index 0000000..4bac02f Binary files /dev/null and b/app/lib/snail_mail/assets/fonts/noto sans regular.ttf differ diff --git a/app/lib/snail_mail/assets/images/acon-joyous-cat.png b/app/lib/snail_mail/assets/images/acon-joyous-cat.png new file mode 100644 index 0000000..4749e19 Binary files /dev/null and b/app/lib/snail_mail/assets/images/acon-joyous-cat.png differ diff --git a/app/lib/snail_mail/assets/images/athena/logo-stars.png b/app/lib/snail_mail/assets/images/athena/logo-stars.png new file mode 100644 index 0000000..5c046bb Binary files /dev/null and b/app/lib/snail_mail/assets/images/athena/logo-stars.png differ diff --git a/app/lib/snail_mail/assets/images/athena/nyc-orphy.png b/app/lib/snail_mail/assets/images/athena/nyc-orphy.png new file mode 100644 index 0000000..f26b7b6 Binary files /dev/null and b/app/lib/snail_mail/assets/images/athena/nyc-orphy.png differ diff --git a/app/lib/snail_mail/assets/images/dino-waving.png b/app/lib/snail_mail/assets/images/dino-waving.png new file mode 100644 index 0000000..b445ca2 Binary files /dev/null and b/app/lib/snail_mail/assets/images/dino-waving.png differ diff --git a/app/lib/snail_mail/assets/images/eleeza-mail-orpheus.png b/app/lib/snail_mail/assets/images/eleeza-mail-orpheus.png new file mode 100644 index 0000000..4b535eb Binary files /dev/null and b/app/lib/snail_mail/assets/images/eleeza-mail-orpheus.png differ diff --git a/app/lib/snail_mail/assets/images/hackatime/badge.png b/app/lib/snail_mail/assets/images/hackatime/badge.png new file mode 100644 index 0000000..e65e387 Binary files /dev/null and b/app/lib/snail_mail/assets/images/hackatime/badge.png differ diff --git a/app/lib/snail_mail/assets/images/hackatime/its_about_time.png b/app/lib/snail_mail/assets/images/hackatime/its_about_time.png new file mode 100644 index 0000000..d11cb80 Binary files /dev/null and b/app/lib/snail_mail/assets/images/hackatime/its_about_time.png differ diff --git a/app/lib/snail_mail/assets/images/hcb/hcb-icon.png b/app/lib/snail_mail/assets/images/hcb/hcb-icon.png new file mode 100644 index 0000000..a910477 Binary files /dev/null and b/app/lib/snail_mail/assets/images/hcb/hcb-icon.png differ diff --git a/app/lib/snail_mail/assets/images/hcpcxc_ra.png b/app/lib/snail_mail/assets/images/hcpcxc_ra.png new file mode 100644 index 0000000..f119011 Binary files /dev/null and b/app/lib/snail_mail/assets/images/hcpcxc_ra.png differ diff --git a/app/lib/snail_mail/assets/images/kestrel-mail-heidi.png b/app/lib/snail_mail/assets/images/kestrel-mail-heidi.png new file mode 100644 index 0000000..3cbc4c6 Binary files /dev/null and b/app/lib/snail_mail/assets/images/kestrel-mail-heidi.png differ diff --git a/app/lib/snail_mail/assets/images/lilia-hcb-stickers-bg.png b/app/lib/snail_mail/assets/images/lilia-hcb-stickers-bg.png new file mode 100644 index 0000000..2a4c8e1 Binary files /dev/null and b/app/lib/snail_mail/assets/images/lilia-hcb-stickers-bg.png differ diff --git a/app/lib/snail_mail/assets/images/msw-heidi-cant-readme.png b/app/lib/snail_mail/assets/images/msw-heidi-cant-readme.png new file mode 100644 index 0000000..8715a9f Binary files /dev/null and b/app/lib/snail_mail/assets/images/msw-heidi-cant-readme.png differ diff --git a/app/lib/snail_mail/assets/images/no_indicium.png b/app/lib/snail_mail/assets/images/no_indicium.png new file mode 100644 index 0000000..d25386e Binary files /dev/null and b/app/lib/snail_mail/assets/images/no_indicium.png differ diff --git a/app/lib/snail_mail/assets/images/speech-tail.png b/app/lib/snail_mail/assets/images/speech-tail.png new file mode 100644 index 0000000..2f1325a Binary files /dev/null and b/app/lib/snail_mail/assets/images/speech-tail.png differ diff --git a/app/lib/snail_mail/assets/images/speech_bubble.png b/app/lib/snail_mail/assets/images/speech_bubble.png new file mode 100644 index 0000000..0e03258 Binary files /dev/null and b/app/lib/snail_mail/assets/images/speech_bubble.png differ diff --git a/app/lib/snail_mail/assets/images/stegg-box-dino.png b/app/lib/snail_mail/assets/images/stegg-box-dino.png new file mode 100644 index 0000000..a6c805e Binary files /dev/null and b/app/lib/snail_mail/assets/images/stegg-box-dino.png differ diff --git a/app/lib/snail_mail/assets/images/tarot/msw-joker.png b/app/lib/snail_mail/assets/images/tarot/msw-joker.png new file mode 100644 index 0000000..0402b44 Binary files /dev/null and b/app/lib/snail_mail/assets/images/tarot/msw-joker.png differ diff --git a/app/lib/snail_mail/base_template.rb b/app/lib/snail_mail/base_template.rb new file mode 100644 index 0000000..344f7ed --- /dev/null +++ b/app/lib/snail_mail/base_template.rb @@ -0,0 +1,228 @@ +module SnailMail + class BaseTemplate + include SnailMail::Helpers + + # Template sizes in points [width, height] + SIZES = { + standard: [6 * 72, 4 * 72], # 4x6 inches (432 x 288 points) + envelope: [9.5 * 72, 4.125 * 72], # #10 envelope (684 x 297 points) + half_letter: [8 * 72, 5 * 72], # half-letter (576 x 360 points) + }.freeze + + # Template metadata - override in subclasses + def self.template_name + name.demodulize.underscore.sub(/_template$/, "") + end + + def self.template_size + :standard # default to 4x6 standard + end + + def self.show_on_single? + false + end + + def self.template_description + "A label template" + end + + # Instance methods + attr_reader :options + + def initialize(options = {}) + @options = options + end + + # Size in points [width, height] + def size + SIZES[self.class.template_size] || SIZES[:standard] + end + + # Main render method, to be implemented by subclasses + def render(pdf, letter) + raise NotImplementedError, "Subclasses must implement the render method" + end + + protected + + # Helper methods for templates + + # Render return address + def render_return_address(pdf, letter, x, y, width, height, options = {}) + default_options = { + font: "arial", + size: 11, + align: :left, + valign: :top, + overflow: :shrink_to_fit, + min_font_size: 6, + } + + options = default_options.merge(options) + font_name = options.delete(:font) + + pdf.font(font_name) do + pdf.text_box( + format_return_address(letter, options[:no_name_line]), + at: [x, y], + width: width, + height: height, + **options, + ) + end + end + + # Render destination address + def render_destination_address(pdf, letter, x, y, width, height, options = {}) + default_options = { + font: "f25", + size: 11, + align: :left, + valign: :center, + overflow: :shrink_to_fit, + min_font_size: 6, + disable_wrap_by_char: true, + } + + options = default_options.merge(options) + font_name = options.delete(:font) + stroke = options.delete(:dbg_stroke) + + pdf.font(font_name) do + pdf.text_box( + letter.address.snailify(letter.return_address.country), + at: [x, y], + width: width, + height: height, + **options, + ) + end + if stroke + pdf.stroke { pdf.rectangle([x, y], width, height) } + end + end + + # Render Intelligent Mail barcode + def render_imb(pdf, letter, x, y, width, options = {}) + + # we want an IMb if: + # - the letter is US-to-US (end-to-end IV) + # - the letter is US-to-non-US (IV until it's out of the US) + # - the letter is non-US-to-US (IV after it enters the US) + # but not if + # - the letter is non-US-to-non-US (that'd be pretty stupid) + + return unless letter.address.us? || letter.return_address.us? + + default_options = { + font: "imb", + size: 24, + align: :center, + overflow: :shrink_to_fit, + } + + options = default_options.merge(options) + font_name = options.delete(:font) + + pdf.font(font_name) do + pdf.text_box( + generate_imb(letter), + at: [x, y], + width: width, + disable_wrap_by_char: true, + **options, + ) + end + end + + # Render QR code + def render_qr_code(pdf, letter, x, y, size = 70) + return unless options[:include_qr_code] + SnailMail::QRCodeGenerator.generate_qr_code(pdf, "https://hack.club/#{letter.public_id}", x, y, size) + pdf.font("f25") do + pdf.text_box("scan this so we know you got it!", at: [x + 3, y + 22], width: 54, size: 6.4) + end + end + + def render_letter_id(pdf, letter, x, y, size, opts = {}) + return if options[:include_qr_code] + pdf.font(opts.delete(:font) || "f25") { pdf.text_box(letter.public_id, at: [x, y], size:, overflow: :shrink_to_fit, valign: :top, **opts) } + end + + private + + # Format destination address + def format_destination_address(letter) + letter.address.snailify(letter.return_address.country) + end + + # Generate IMb barcode + def generate_imb(letter) + # Use the IMb module to generate the barcode + IMb.new(letter).generate + end + + def render_postage(pdf, letter, x = pdf.bounds.right - 138) + if letter.postage_type == "indicia" + IMI.render_indicium(pdf, letter, letter.usps_indicium, x) + FIM.render_fim_d(pdf, x - 62) + elsif letter.postage_type == "stamps" + postage_amount = letter.postage + stamps = USPS::McNuggetEngine.find_stamp_combination(postage_amount) + + requested_stamps = if stamps.size == 1 + stamp = stamps.first + "#{stamp[:count]} #{stamp[:name]}" + elsif stamps.size == 2 + "#{stamps[0][:count]} #{stamps[0][:name]} and #{stamps[1][:count]} #{stamps[1][:name]}" + else + stamps.map.with_index do |stamp, index| + if index == stamps.size - 1 + "and #{stamp[:count]} #{stamp[:name]}" + else + "#{stamp[:count]} #{stamp[:name]}" + end + end.join(", ") + end + + postage_info = <<~EOT + i take #{ActiveSupport::NumberHelper.number_to_currency(postage_amount)} in postage, so #{requested_stamps} + EOT + + pdf.bounding_box([pdf.bounds.right - 55, pdf.bounds.top - 5], width: 50, height: 50) do + pdf.font("f25") do + pdf.text_box( + postage_info, + at: [1, 48], + width: 48, + height: 45, + size: 8, + align: :center, + min_font_size: 4, + overflow: :shrink_to_fit, + ) + end + end + else + pdf.bounding_box([pdf.bounds.right - 55, pdf.bounds.top - 5], width: 52, height: 50) do + pdf.font("f25") do + pdf.text_box("please affix however much postage your post would like", at: [1, 48], width: 50, height: 45, size: 8, align: :center, min_font_size: 4, overflow: :shrink_to_fit) + end + end + end + end + + def format_return_address(letter, no_name_line = false) + return_address = letter.return_address + return "No return address" unless return_address + + <<~EOA + #{letter.return_address_name_line unless no_name_line} + #{[return_address.line_1, return_address.line_2].compact_blank.join("\n")} + #{return_address.city}, #{return_address.state} #{return_address.postal_code} + #{return_address.country if return_address.country != letter.address.country} + EOA + .strip + end + end +end diff --git a/app/lib/snail_mail/fim.rb b/app/lib/snail_mail/fim.rb new file mode 100644 index 0000000..765bbe8 --- /dev/null +++ b/app/lib/snail_mail/fim.rb @@ -0,0 +1,26 @@ +module SnailMail + module FIM + class << self + FIM_D = [ + 1, 1, 1, 0, 1, 0, 1, 1, 1 + ] + + FIM_HEIGHT = 45 + FIM_ELEMENT_WIDTH = 2.25 + + def render_fim_d(pdf, x = 245) + render_fim(pdf, FIM_D, x) + end + + def render_fim(pdf, fim, x) + pdf.fill do + lx = 0 + fim.each do |e| + pdf.rectangle([ x + lx, pdf.bounds.height ], FIM_ELEMENT_WIDTH, FIM_HEIGHT) if e == 1 + lx += FIM_ELEMENT_WIDTH * 2 # 1 for bar, 1 for spacer + end + end + end + end + end +end diff --git a/app/lib/snail_mail/helpers.rb b/app/lib/snail_mail/helpers.rb new file mode 100644 index 0000000..8ebeffd --- /dev/null +++ b/app/lib/snail_mail/helpers.rb @@ -0,0 +1,7 @@ +module SnailMail + module Helpers + def image_path(image_name) + File.join(Rails.root, "app", "lib", "snail_mail", "assets", "images", image_name) + end + end +end diff --git a/app/lib/snail_mail/imb.rb b/app/lib/snail_mail/imb.rb new file mode 100644 index 0000000..bff4467 --- /dev/null +++ b/app/lib/snail_mail/imb.rb @@ -0,0 +1,35 @@ +module SnailMail + class IMb + attr_reader :letter + + def initialize(letter) + @letter = letter + end + + def generate + barcode_id = "00" # no OEL + stid = "310" # no address corrections – no printed endorsements, but! IV-MTR! + mailer_id = letter.usps_mailer_id&.mid + return "" unless mailer_id + serial_number = letter.imb_serial_number + routing_code = letter.address.us? ? letter.address.postal_code.gsub(/[^0-9]/, "") : nil # zip(+dpc?) but no dash + + routing_code = nil unless [5, 9, 11].include?(routing_code&.length) + + begin + Imb::Barcode.new( + barcode_id, + stid, + mailer_id, + serial_number, + routing_code + ).barcode_letters + rescue ArgumentError => e + Rails.logger.warn("Bad IMb input: #{e.message} @ MID #{mailer_id} SN #{serial_number} RC #{routing_code}") + uuid = Honeybadger.notify(e) + Rails.logger.warn("IMb error (please report EID: #{uuid})") + "" + end + end + end +end diff --git a/app/lib/snail_mail/imi.rb b/app/lib/snail_mail/imi.rb new file mode 100644 index 0000000..2582b33 --- /dev/null +++ b/app/lib/snail_mail/imi.rb @@ -0,0 +1,55 @@ +module SnailMail + class IMI + class << self + include SnailMail::Helpers + + def render_indicium(pdf, letter, indicium, x = bounds.width - 294, options = {}) + svg = Nokogiri::XML(indicium&.svg || (raise "no indicium?")) + imi_el = svg % "image#imi-barcode" + raise "no imi element?" unless imi_el.present? + imi_href = imi_el["xlink:href"] + raise "no IMI href???" unless imi_href.present? + imi_png = Base64.decode64(imi_href.split(",", 2).last) + raise "failed to decode imi png -_-" unless imi_png.present? + pdf.image( + StringIO.new(imi_png), + at: [x + 79, pdf.bounds.top - 8], + width: 51, + ) + pdf.bounding_box([x, pdf.bounds.top - 11], width: 90, height: 45) do + pdf.font_size(8) + pdf.text("U.S. POSTAGE", style: :bold) + pdf.text(letter.mailing_date.strftime("%m/%d/%Y")) unless options[:no_indicia_date] + pdf.text(ActiveSupport::NumberHelper.number_to_currency(indicium.cost)) + pdf.text("LFP", style: :bold) + pdf.font("f25") { pdf.text("#{Rails.env.production? ? "hackapost" : "DEV"}!/#{indicium.hashid}", size: 6.2) } + end + + pdf.image(StringIO.new(WATERMARK), at: [x, pdf.bounds.top - 5], width: 121) if (wm = svg % "#watermark") && wm["visibility"] != "hidden" + rescue StandardError => e + render_no_indicium(pdf, letter, e.class == StandardError ? e.message : "#{e.class}: #{e.message}") + end + + private + + def render_no_indicium(pdf, letter, reason) + error_text = <<~EOT + this shouldn't happen. + please tell nora about #{letter.public_id} and #{letter.usps_indicium&.public_id || "nonexistent indicium!"} :-( + "#{reason}" + EOT + + pdf.image( + image_path("no_indicium.png"), + width: 144, + at: [432 - 144, 288], + ) + pdf.text_box(error_text, at: [256, 280], width: 90, height: 65, overflow: :shrink_to_fit, size: 8) + end + + WATERMARK = Base64.decode64 <<~EOI + iVBORw0KGgoAAAANSUhEUgAAAfQAAADiCAQAAAASJ/0zAAADE2lDQ1BEb3QgR2FpbiAyMCUAACiRY2BgnuDo4uTKJMDAUFBUUuQe5BgZERmlwH6egY2BmYGBgYGBITG5uMAxIMCHgYGBIS8/L5UBFTAyMHy7xsDIwMDAcFnX0cXJlYE0wJpcUFTCwMBwgIGBwSgltTiZgYHhCwMDQ3p5SUEJAwNjDAMDg0hSdkEJAwNjAQMDg0h2SJAzAwNjCwMDE09JakUJAwMDg3N+QWVRZnpGiYKhpaWlgmNKflKqQnBlcUlqbrGCZ15yflFBflFiSWoKAwMD1A4GBgYGXpf8EgX3xMw8BSMDVRLdTRBEREYpQFiI8EGIIUByaVEZhMXIwMDAIMCgwGDA4MAQwJDIUM+wgOEowxtGcUYXxlLGFYz3mMSYgpgmMF1gFmaOZF7I/IbFkqWD5RarHmsr6z02S7ZpbN/Yw9l3cyhxdHF84UzkvMDlyLWFW5N7AY8Uz1ReId5JfMJ80/hl+BcL6AjsEHQVvCKUKvRDuFdERWSvaLjoF7FJ4kbiVyQqJOUkj0nlS0tLn5Apk1WXvSXXJ+8i/0dhq2Khkp7SW+W1KgWqJqo/1Q6qd2mEaippftA6oD1JJ1XXSk9Q75X+EYMFhrVGMca2JvKmzKYvzS6Y77RYYjnBqs461ybONtDO1d7awdhRx0nNWclFwVXeTcFd2UPdU9fLxNvGx9032C/BPz+gPnBi0NLgXSEXQ1+GM0XIRVpFRURXxMyM3RP3IIEtUTcpLLkhZU3qzXSODIvMzKy52Rdz2fPs8ysKNhW+K9YuySpdVfamQr+ypGpXDWOtV93U+oeNek01zWdb5doK2492SncVdZ/uVe1r7L870WbS7Ml/p8ZPOzxDY2b/rO9zEuaenm++YOkikcWtS74ty1x+b2XIqtNrXNbuW2+5Ydsmk81btpps277Dauf+3a57zu4L2//gYM6hn0faj4kfX3HS+tS5M8lnf52fdFH70tEriVf/XZ9z0+bW3Tv195Tvn3iY91jsyf5nmS9EXh58nf9W/t2FD02fTD+/+rrge/hPgV+n/rT+c/z/HwANAA803sW02gAAAAlwSFlzAAAOxAAADsQBlSsOGwAABwdpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDkuMS1jMDAyIDc5LmExY2QxMmY0MSwgMjAyNC8xMS8wOC0xNjowOToyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMSIgcGhvdG9zaG9wOkRhdGVDcmVhdGVkPSIyMDI1LTA0LTA3VDE1OjU5OjAzLTA0OjAwIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyNS0wNC0wN1QxNTo1OTowMy0wNDowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyNS0wNC0wN1QxNjowNToyNi0wNDowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjUtMDQtMDdUMTY6MDU6MjYtMDQ6MDAiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MGQ5N2ViMTAtZTg2MC00ODQ5LThlZGUtMTY2MTkwNjU3MDRiIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjBkOTdlYjEwLWU4NjAtNDg0OS04ZWRlLTE2NjE5MDY1NzA0YiIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjBkOTdlYjEwLWU4NjAtNDg0OS04ZWRlLTE2NjE5MDY1NzA0YiIgdGlmZjpJbWFnZVdpZHRoPSI1MDAiIHRpZmY6SW1hZ2VMZW5ndGg9IjIyNiIgdGlmZjpYUmVzb2x1dGlvbj0iOTYvMSIgdGlmZjpZUmVzb2x1dGlvbj0iOTYvMSIgdGlmZjpSZXNvbHV0aW9uVW5pdD0iMiIgZXhpZjpDb2xvclNwYWNlPSIxIiBleGlmOlBpeGVsWERpbWVuc2lvbj0iNTAwIiBleGlmOlBpeGVsWURpbWVuc2lvbj0iMjI2IiBkYzpmb3JtYXQ9ImltYWdlL3BuZyI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSB4bXBNTTphY3Rpb249InByb2R1Y2VkIiB4bXBNTTpzb2Z0d2FyZUFnZW50PSJBZmZpbml0eSBEZXNpZ25lciAyIDIuNC4wIiB4bXBNTTp3aGVuPSIyMDI1LTA0LTA3VDE1OjU5OjUwLTA0OjAwIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJwcm9kdWNlZCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWZmaW5pdHkgUGhvdG8gMiAyLjEuMSIgc3RFdnQ6d2hlbj0iMjAyNS0wNC0wN1QxNjowMTo1Ny0wNDowMCIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MGQ5N2ViMTAtZTg2MC00ODQ5LThlZGUtMTY2MTkwNjU3MDRiIiBzdEV2dDp3aGVuPSIyMDI1LTA0LTA3VDE2OjA1OjI2LTA0OjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjYuMiAoTWFjaW50b3NoKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4Kb33YAAArvUlEQVR4nO1d7Zasqg7Uu877v7L3x0y3kFSSSkDbnk2d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 + EOI + end + end +end diff --git a/app/lib/snail_mail/label_generator.rb b/app/lib/snail_mail/label_generator.rb new file mode 100644 index 0000000..2806b3a --- /dev/null +++ b/app/lib/snail_mail/label_generator.rb @@ -0,0 +1,105 @@ +require "prawn" +require "securerandom" +require_relative "templates" + +module SnailMail + class LabelGenerator + class Error < StandardError; end + + attr_reader :options + + def initialize(options = {}) + @options = { + margin: 0, + }.merge(options) + end + + # Generate labels for a collection of letters + # template_names: array of template names to cycle through + # Returns the PDF object directly (doesn't write to disk unless output_path provided) + def generate(letters, output_path = nil, template_names) + raise Error, "No letters provided" if letters.empty? + raise Error, "No template names provided" if template_names.empty? + + begin + # Get template classes upfront to avoid repeated lookups + template_classes = template_names.map do |name| + Templates.get_template_class(name) + end + + # Ensure all template sizes are the same + sizes = template_classes.map(&:template_size).uniq + if sizes.length > 1 + raise Error, "Mixed template sizes in batch (#{sizes.join(", ")}). All templates must have the same size." + end + + # Create template lookup for faster access + template_lookup = {} + template_names.each_with_index do |name, i| + template_lookup[name] = template_classes[i] + end + + # All templates have the same size, create one PDF + pdf = create_document(sizes.first) + + letters.each_with_index do |letter, index| + template_name = template_names[index % template_names.length] + render_letter(pdf, letter, template: template_name, template_class: template_lookup[template_name]) + pdf.start_new_page unless index == letters.length - 1 + end + + # Write to disk only if output_path is provided + pdf.render_file(output_path) if output_path + + # Return the PDF object + pdf + rescue => e + Rails.logger.error("Failed to generate labels: #{e.message}") + raise + end + end + + private + + def create_document(page_size_name) + page_size = BaseTemplate::SIZES[page_size_name] || BaseTemplate::SIZES[:standard] + + pdf = Prawn::Document.new( + page_size: page_size, + margin: @options[:margin], + ) + + register_fonts(pdf) + pdf.fallback_fonts(["arial", "noto"]) + pdf + end + + def register_fonts(pdf) + pdf.font_families.update( + "comic" => { normal: font_path("comic sans.ttf") }, + "arial" => { normal: font_path("arial.otf") }, + "f25" => { normal: font_path("f25.ttf") }, + "imb" => { normal: font_path("imb.ttf") }, + "gohu" => { normal: font_path("gohu.ttf") }, + "noto" => { normal: font_path("noto sans regular.ttf") }, + ) + end + + def font_path(font_name) + File.join(Rails.root, "app", "lib", "snail_mail", "assets", "fonts", font_name) + end + + def render_letter(pdf, letter, letter_options = {}) + template_options = @options.merge(letter_options) + + # Use pre-fetched template class if provided, otherwise look it up + if template_class = letter_options[:template_class] + template = template_class.new(template_options) + else + template = Templates.template_for(letter, template_options) + end + + template.render(pdf, letter) + end + end +end diff --git a/app/lib/snail_mail/preview.rb b/app/lib/snail_mail/preview.rb new file mode 100644 index 0000000..57f4ec5 --- /dev/null +++ b/app/lib/snail_mail/preview.rb @@ -0,0 +1,104 @@ +require "open3" +require "rmagick" + +module SnailMail + module Preview + OUTPUT_DIR = Rails.root.join("app", "frontend", "images", "template_previews") + + class FakeAddress < OpenStruct + def us_format + <<~EOA + #{name_line} + #{[line_1, line_2].compact_blank.join("\n")} + #{city}, #{state} #{postal_code} + #{country} + EOA + end + + def us? + country == "US" + end + + def snailify(origin = "US") + SnailButNbsp.new( + name: name_line, + line_1:, + line_2: line_2.presence, + city:, + region: state, + postal_code:, + country: country, + origin: origin, + ).to_s + end + end + + def self.generate_previews + OUTPUT_DIR.mkpath + + return_address = OpenStruct.new( + name: "Hack Club", + line_1: "15 Falls Rd", + city: "Shelburne", + state: "VT", + postal_code: "05482", + country: "US", + ) + + names = [ + "Orpheus", + "Heidi Hakkuun", + "Dinobox", + "Arcadius", + "Cap'n Trashbeard", + ] + + usps_mailer_id = OpenStruct.new(mid: "111111") + + Templates.available_templates.each do |name| + template = Templates.get_template_class(name) + sender, recipient = names.sample(2) + + mock_letter = OpenStruct.new( + address: FakeAddress.new( + line_1: "8605 Santa Monica Blvd", + line_2: "Apt. 86294", + city: "West Hollywood", + state: "CA", + postal_code: "90069", + country: "US", + name_line: sender, + ), + return_address:, + return_address_name_line: recipient, + postage_type: "stamps", + postage: 0.73, + usps_mailer_id:, + imb_serial_number: "1337", + metadata: {}, + rubber_stamps: "here's where rubber stamps go!", + public_id: "ltr!PR3V13W", + ) + + Rails.logger.info("generating preview for #{name}...") + pdf = SnailMail::Service.generate_label(mock_letter, template: name) + + png_path = OUTPUT_DIR.join("#{template.name.split("::").last.underscore}.png") + + begin + image = Magick::Image.from_blob(pdf.render) do |i| + i.density = 300 + end.first + + image.alpha(Magick::RemoveAlphaChannel) + image.background_color = "white" + image.pixel_interpolation_method = Magick::IntegerInterpolatePixel + image.write(png_path) + rescue => e + Rails.logger.error("Failed to convert PDF to PNG: #{e.message}") + raise e + end + end + end + end +end diff --git a/app/lib/snail_mail/qr_code_generator.rb b/app/lib/snail_mail/qr_code_generator.rb new file mode 100644 index 0000000..ceed0e2 --- /dev/null +++ b/app/lib/snail_mail/qr_code_generator.rb @@ -0,0 +1,45 @@ +require "rqrcode" +require "prawn" +require "stringio" + +module SnailMail + class QRCodeGenerator + DEFAULT_SIZE = 80 + DEFAULT_QR_OPTIONS = { + bit_depth: 1, + border_modules: 1, + color_mode: ChunkyPNG::COLOR_GRAYSCALE, + color: "black", + file: nil, + fill: "white", + module_px_size: 6, + resize_gte_to: false, + resize_exactly_to: false, + size: 120, + }.freeze + + # Generate a QR code and add it to the PDF + def self.generate_qr_code(pdf, content, x, y, size = DEFAULT_SIZE) + raise ArgumentError, "PDF document cannot be nil" unless pdf + raise ArgumentError, "QR code content cannot be empty" if content.to_s.empty? + + begin + qr = RQRCode::QRCode.new(content) + png = qr.as_png(DEFAULT_QR_OPTIONS) + + # Use StringIO instead of tempfile + io = StringIO.new + png.write(io) + io.rewind + + # Add the PNG to the PDF without creating a file + pdf.image io, at: [x, y], width: size, height: size + rescue => e + Rails.logger.error("QR code generation failed: #{e.message}") + uuid = Honeybadger.notify(e) + # Fallback to a text label if QR code fails + pdf.text_box "QR Error (error: #{uuid})", at: [x, y], width: size, height: size + end + end + end +end diff --git a/app/lib/snail_mail/service.rb b/app/lib/snail_mail/service.rb new file mode 100644 index 0000000..da30341 --- /dev/null +++ b/app/lib/snail_mail/service.rb @@ -0,0 +1,121 @@ +require_relative "label_generator" +require_relative "templates" + +module SnailMail + class Service + class Error < StandardError; end + + # Generate a label for a single letter and attach it to the letter + def self.generate_label(letter, options = {}) + validate_letter(letter) + template_name = options.delete(:template) || default_template + + generator = LabelGenerator.new(options) + + # Generate PDF without writing to disk + pdf = generator.generate([letter], nil, [template_name]) + + pdf + end + + # Generate labels for a batch of letters and attach to batch + def self.generate_batch_labels(letters, options = {}) + validate_batch(letters) + + template_cycle = options[:template_cycle] + validate_template_cycle(template_cycle) if template_cycle + + # If no template cycle is provided, use the default template + template_cycle ||= [default_template] + + # Get template classes once, avoid repeated lookups + template_classes = template_cycle.map do |name| + Templates.get_template_class(name) + end + + # Ensure all templates in the cycle are of the same size + template_sizes = template_classes.map(&:template_size).uniq + if template_sizes.length > 1 + raise Error, "All templates in cycle must have the same size. Found: #{template_sizes.join(", ")}" + end + + # Generate labels with template cycling without writing to disk + generator = LabelGenerator.new(options) + pdf = generator.generate(letters, nil, template_cycle) + + pdf + end + + # List available templates + def self.available_templates + Templates.available_templates.uniq + end + + # Get a list of all templates with their metadata + def self.template_info + Templates.all.map do |template_class| + { + name: template_class.template_name.to_sym, + size: template_class.template_size, + description: template_class.template_description, + is_default: template_class == Templates::DEFAULT_TEMPLATE, + } + end + end + + # Get templates for a specific size + def self.templates_for_size(size) + templates = Templates.templates_by_size(size) + Rails.logger.info "Templates for size #{size}: Found #{templates.count} templates" + + template_names = templates.map do |template_class| + begin + name = template_class.template_name.to_s + Rails.logger.info " - Template: #{name}, Size: #{template_class.template_size}" + name + rescue => e + Rails.logger.error "Error getting template name: #{e.message}" + nil + end + end.compact + + Rails.logger.info "Final template names for size #{size}: #{template_names.inspect}" + template_names + end + + # Get the default template + def self.default_template + Templates::DEFAULT_TEMPLATE.template_name + end + + # Check if templates exist + def self.templates_exist?(template_names) + Array(template_names).all? do |name| + Templates.template_exists?(name) + end + end + + private + + def self.validate_letter(letter) + raise Error, "Letter cannot be nil" unless letter + raise Error, "Letter must have an address" unless letter.respond_to?(:address) && letter.address + end + + def self.validate_batch(letters) + raise Error, "Letters cannot be nil" unless letters + raise Error, "Letters must be a collection" unless letters.respond_to?(:each) + raise Error, "Letters collection cannot be empty" if letters.empty? + end + + def self.validate_template_cycle(template_cycle) + raise Error, "Template cycle must be an array" unless template_cycle.is_a?(Array) + raise Error, "Template cycle cannot be empty" if template_cycle.empty? + + invalid_templates = template_cycle.reject { |name| templates_exist?([name]) } + if invalid_templates.any? + raise Error, "Invalid templates in cycle: #{invalid_templates.join(", ")}" + end + end + end +end diff --git a/app/lib/snail_mail/templates.rb b/app/lib/snail_mail/templates.rb new file mode 100644 index 0000000..b862d66 --- /dev/null +++ b/app/lib/snail_mail/templates.rb @@ -0,0 +1,85 @@ +module SnailMail + module Templates + class TemplateNotFoundError < StandardError; end + + # All available templates hardcoded in a single array + TEMPLATES = [ + JoyousCatTemplate, + MailOrpheusTemplate, + HCBStickersTemplate, + KestrelHeidiTemplate, + # HackatimeStickersTemplate, + TarotTemplate, + DinoWavingTemplate, + HcpcxcTemplate, + HackatimeTemplate, + HeidiReadmeTemplate, + GoodJobTemplate, + HackatimeOTPTemplate, + AthenaStickersTemplate, + HCBWelcomePostcardTemplate, + ].freeze + + # Default template to use when none is specified + DEFAULT_TEMPLATE = KestrelHeidiTemplate + + class << self + # Get all template classes + def all + TEMPLATES + end + + # Get a template class by name + def get_template_class(name) + template_name = name.to_sym + template_class = TEMPLATES.find { |t| t.template_name.to_sym == template_name } + template_class || raise(TemplateNotFoundError, "Template not found: #{name}") + end + + # Get a template instance for a letter + # Options: + # template: Specifies the template to use, overriding any template in letter.rubber_stamps + # template_class: Pre-fetched template class to use (fastest option) + def template_for(letter, options = {}) + # First check if template_class is provided (fastest path) + if template_class = options[:template_class] + return template_class.new(options) + end + + # Next check if template name is specified in options + template_name = options[:template]&.to_sym + + template_class = if template_name + # Find template by name + TEMPLATES.find { |t| t.template_name.to_sym == template_name } + else + # Use default + DEFAULT_TEMPLATE + end + + # Create a new instance of the template + template_class ? template_class.new(options) : DEFAULT_TEMPLATE.new(options) + end + + # Get templates by size + def templates_by_size(size) + size_sym = size.to_sym + TEMPLATES.select { |t| t.template_size == size_sym } + end + + # List all available template names + def available_templates + TEMPLATES.map { |t| t.template_name.to_sym } + end + + def available_single_templates + TEMPLATES.select { |t| t.show_on_single? }.map { |t| t.template_name.to_sym } + end + + # Check if a template exists + def template_exists?(name) + TEMPLATES.any? { |t| t.template_name.to_sym == name.to_sym } + end + end + end +end diff --git a/app/lib/snail_mail/templates/athena_stickers_template.rb b/app/lib/snail_mail/templates/athena_stickers_template.rb new file mode 100644 index 0000000..c1a85b2 --- /dev/null +++ b/app/lib/snail_mail/templates/athena_stickers_template.rb @@ -0,0 +1,53 @@ +module SnailMail + module Templates + class AthenaStickersTemplate < BaseTemplate + def self.template_name + "Athena stickers" + end + + def self.show_on_single? + true + end + + def render(pdf, letter) + render_return_address(pdf, letter, 5, pdf.bounds.top - 45, 190, 90, size: 8, font: "f25") + pdf.image( + image_path("athena/logo-stars.png"), + at: [5, pdf.bounds.top - 5], + width: 80, + ) + render_destination_address( + pdf, + letter, + 104, + 196, + 256, + 107, + { size: 18, valign: :center, align: :left } + ) + + pdf.stroke do + pdf.line_width = 2.5 + pdf.rounded_rectangle([72, 202], 306, 122, 10) + end + + pdf.image( + image_path("athena/nyc-orphy.png"), + at: [13, 98], + height: 97, + ) + + pdf.image( + image_path("speech-tail.png"), + at: [96, 83], + width: 32.2, + ) + + render_imb(pdf, letter, 230, 25, 190) + render_letter_id(pdf, letter, 3, 15, 8, rotate: 90) + render_qr_code(pdf, letter, 7, 160, 50) + render_postage(pdf, letter) + end + end + end +end diff --git a/app/lib/snail_mail/templates/character_template.rb b/app/lib/snail_mail/templates/character_template.rb new file mode 100644 index 0000000..686e9cf --- /dev/null +++ b/app/lib/snail_mail/templates/character_template.rb @@ -0,0 +1,64 @@ +require_relative "../base_template" + +module SnailMail + module Templates + class CharacterTemplate < BaseTemplate + # Abstract base class for character templates + + def self.template_name + "character" # This template isn't meant to be used directly + end + + def self.template_description + "Base class for character templates (not for direct use)" + end + + attr_reader :character_image, :speech_bubble_image, :character_position, :speech_position + + def initialize(options = {}) + super + @character_image = options[:character_image] + @speech_bubble_image = options[:speech_bubble_image] || "speech_bubble.png" + @character_position = options[:character_position] || { x: 10, y: 100, width: 120 } + @speech_position = options[:speech_position] || { x: 100, y: 250, width: 300, height: 100 } + end + + def render(pdf, letter) + # Render character + pdf.image( + image_path(character_image), + at: [ character_position[:x], character_position[:y] ], + width: character_position[:width] + ) + + # Render speech bubble + pdf.image( + image_path(speech_bubble_image), + at: [ speech_position[:x], speech_position[:y] ], + width: speech_position[:width] + ) + + # Render return address + render_return_address(pdf, letter, 10, 270, 130, 70) + + # Render destination address in speech bubble + render_destination_address( + pdf, + letter, + speech_position[:x] + 20, + speech_position[:y] - 10, + speech_position[:width] - 40, + speech_position[:height] - 20, + { size: 12, valign: :center } + ) + + # Render IMb barcode + render_imb(pdf, letter, 100, 90, 280, 30) + + # Render QR code for tracking + render_qr_code(pdf, letter, 5, 65, 60) + render_postage(pdf, letter) + end + end + end +end diff --git a/app/lib/snail_mail/templates/corporate_envelope_template.rb b/app/lib/snail_mail/templates/corporate_envelope_template.rb new file mode 100644 index 0000000..8563557 --- /dev/null +++ b/app/lib/snail_mail/templates/corporate_envelope_template.rb @@ -0,0 +1,59 @@ +require_relative "../base_template" + +module SnailMail + module Templates + class CorporateEnvelopeTemplate < BaseTemplate + def self.template_name + "corporate_envelope" + end + + def self.template_size + :envelope # Use the envelope size from BaseTemplate::SIZES + end + + def self.template_description + "Professional business envelope template" + end + + def render(pdf, letter) + # Draw a subtle border + pdf.stroke do + pdf.rectangle [ 15, 4 * 72 - 15 ], 9.5 * 72 - 30, 4.125 * 72 - 30 + end + + # Render return address in top left + pdf.font("Helvetica") do + pdf.text_box( + format_return_address(letter), + at: [ 30, 4 * 72 - 30 ], + width: 250, + height: 60, + overflow: :shrink_to_fit, + min_font_size: 8, + style: :bold + ) + end + + # Render destination address + pdf.font("Helvetica") do + pdf.text_box( + format_destination_address(letter), + at: [ 4.5 * 72 - 200, 2.5 * 72 + 50 ], + width: 400, + height: 120, + overflow: :shrink_to_fit, + min_font_size: 10, + leading: 2 + ) + end + + # Render IMb barcode at bottom + render_imb(pdf, letter, 72, 30, 7 * 72, 30) + + # Render QR code in bottom left with smaller size + render_qr_code(pdf, letter, 25, 70, 50) + render_postage(pdf, letter) + end + end + end +end diff --git a/app/lib/snail_mail/templates/dino_waving_template.rb b/app/lib/snail_mail/templates/dino_waving_template.rb new file mode 100644 index 0000000..ddfa10f --- /dev/null +++ b/app/lib/snail_mail/templates/dino_waving_template.rb @@ -0,0 +1,41 @@ +module SnailMail + module Templates + class DinoWavingTemplate < BaseTemplate + def self.template_name + "Dino Waving" + end + + def self.show_on_single? + true + end + + def render(pdf, letter) + pdf.image( + image_path("dino-waving.png"), + at: [333, 163], + width: 87, + ) + + # Render return address + render_return_address(pdf, letter, 10, 278, 260, 70, size: 10) + + # Render destination address in speech bubble + render_destination_address( + pdf, + letter, + 88, + 166, + 236, + 71, + { size: 16, valign: :bottom, align: :left } + ) + + # Render IMb barcode + render_imb(pdf, letter, 240, 24, 183) + render_qr_code(pdf, letter, 5, 65, 60) + render_letter_id(pdf, letter, 10, 19, 10) + render_postage(pdf, letter) + end + end + end +end diff --git a/app/lib/snail_mail/templates/envelope_template.rb b/app/lib/snail_mail/templates/envelope_template.rb new file mode 100644 index 0000000..ec63826 --- /dev/null +++ b/app/lib/snail_mail/templates/envelope_template.rb @@ -0,0 +1,46 @@ +require_relative "../base_template" + +module SnailMail + module Templates + class EnvelopeTemplate < BaseTemplate + def self.template_name + "envelope" + end + + def self.template_size + :envelope # Use the envelope size from BaseTemplate::SIZES + end + + def self.template_description + "Standard #10 business envelope template" + end + + def render(pdf, letter) + # Render return address in top left + render_return_address(pdf, letter, 15, 4 * 72 - 30, 250, 60) + + # Render destination address centered + render_destination_address( + pdf, + letter, + 4.5 * 72 - 150, # Centered horizontally + 2.5 * 72, # Centered vertically + 300, # Width + 120, # Height + { + size: 12, + valign: :center, + align: :center + } + ) + + # Render IMb barcode at bottom + render_imb(pdf, letter, 72, 30, 7 * 72, 30) + + # Render QR code in bottom left + render_qr_code(pdf, letter, 15, 70, 60) + render_postage(pdf, letter) + end + end + end +end diff --git a/app/lib/snail_mail/templates/good_job_template.rb b/app/lib/snail_mail/templates/good_job_template.rb new file mode 100644 index 0000000..8258d72 --- /dev/null +++ b/app/lib/snail_mail/templates/good_job_template.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true +module SnailMail + module Templates + class GoodJobTemplate < HalfLetterTemplate + ADDRESS_FONT = "arial" + def self.template_name + "good job" + end + + def self.template_size + :half_letter + end + + def render_front(pdf, letter) + pdf.font "arial" do + pdf.text_box "good job", size: 99, at: [0, pdf.bounds.top], valign: :center, align: :center + end + + pdf.text_box "from: @#{letter.metadata["gj_from"]}\n#{letter.metadata["gj_reason"]}", size: 18, at: [100, 100], align: :left + end + end + end +end diff --git a/app/lib/snail_mail/templates/hackatime_otp_template.rb b/app/lib/snail_mail/templates/hackatime_otp_template.rb new file mode 100644 index 0000000..fc54f40 --- /dev/null +++ b/app/lib/snail_mail/templates/hackatime_otp_template.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true +module SnailMail + module Templates + class HackatimeOTPTemplate < HalfLetterTemplate + ADDRESS_FONT = "comic" + + def self.template_name + "hackatime OTP" + end + + def self.template_size + :half_letter + end + + def render_front(pdf, letter) + pdf.move_down 100 + pdf.text("Your Hackatime sign-in code is:", style: :bold, size: 30, align: :center) + pdf.move_down 10 + pdf.text(letter.rubber_stamps || "mrrrrp :3", style: :bold, size: 80, align: :center) + # pdf.move_up 30 + pdf.text("This code will expire in 1 year.", size: 10, align: :center) + pdf.text("(that's #{1.year.from_now.in_time_zone("America/New_York").strftime("%-I:%M %p EST on %B %d")})", size: 9, align: :center, style: :italic) + + pdf.stroke_rectangle([2, 55], pdf.bounds.width - 5, 52) + + pdf.bounding_box([5, 50], width: pdf.bounds.width - 15, height: 49) do + pdf.line_width 3 + pdf.text("CONFIDENTIALITY NOTICE:", style: :bold, size: 8) + pdf.text("The information contained in this letter is intended only for the use of the individual named on the address side. It may contain information that is privileged, confidential, and exempt from disclosure under applicable law. If you are not the intended recipient, you are hereby notified that any disclosure, copying, or distribution of this information is prohibited. If you believe you have received this letter in error, please notify us immediately by return postcard and securely destroy the original letter.", size: 8) + end + end + end + end +end diff --git a/app/lib/snail_mail/templates/hackatime_stickers_template.rb b/app/lib/snail_mail/templates/hackatime_stickers_template.rb new file mode 100644 index 0000000..2c1c5ba --- /dev/null +++ b/app/lib/snail_mail/templates/hackatime_stickers_template.rb @@ -0,0 +1,26 @@ +module SnailMail + module Templates + class HackatimeStickersTemplate < KestrelHeidiTemplate + MSG = <<~EOT + you're getting this because you coded for + ≥15 minutes during Scrapyard and tracked + it w/ Hackatime V2! ^_^ + + zach sent you an email about it... + EOT + + def self.template_name + "Hackatime Stickers" + end + + def render(pdf, letter) + super + render_letter_id(pdf, letter, 360, 13, 12) + pdf.image(image_path("hackatime/badge.png"), at: [ 10, 92 ], width: 117) + pdf.font("gohu") do + pdf.text_box(MSG, at: [ 162, 278 ], size: 8) + end + end + end + end +end diff --git a/app/lib/snail_mail/templates/hackatime_template.rb b/app/lib/snail_mail/templates/hackatime_template.rb new file mode 100644 index 0000000..772a5d8 --- /dev/null +++ b/app/lib/snail_mail/templates/hackatime_template.rb @@ -0,0 +1,46 @@ +module SnailMail + module Templates + class HackatimeTemplate < BaseTemplate + def self.template_name + "Hackatime (new)" + end + + def render(pdf, letter) + pdf.image( + image_path("hackatime/its_about_time.png"), + at: [13, 219], + width: 409, + ) + + # Render speech bubble + # pdf.image( + # image_path(speech_bubble_image), + # at: [speech_position[:x], speech_position[:y]], + # width: speech_position[:width] + # ) + + # Render return address + render_return_address(pdf, letter, 10, 278, 146, 70, font: "f25") + + # Render destination address in speech bubble + render_destination_address( + pdf, + letter, + 80, + 134, + 290, + 86, + { size: 19, valign: :top, align: :left } + ) + + # Render IMb barcode + render_imb(pdf, letter, 216, 25, 207) + + render_letter_id(pdf, letter, 10, 19, 10) + render_qr_code(pdf, letter, 5, 55, 50) + + render_postage(pdf, letter) + end + end + end +end diff --git a/app/lib/snail_mail/templates/half_letter_template.rb b/app/lib/snail_mail/templates/half_letter_template.rb new file mode 100644 index 0000000..876eee2 --- /dev/null +++ b/app/lib/snail_mail/templates/half_letter_template.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true +module SnailMail + module Templates + class HalfLetterTemplate < BaseTemplate + ADDRESS_FONT = "f25" + + def self.template_name + raise NotImplementedError, "Subclass must implement template_name" + end + + def self.template_size + :half_letter + end + + def render_front(pdf, letter) + raise NotImplementedError, "Subclass must implement render_front" + end + + def render(pdf, letter) + render_front(pdf, letter) + + pdf.start_new_page + + render_postage(pdf, letter) + + render_return_address(pdf, letter, 10, pdf.bounds.top - 10, 146, 70) + render_imb(pdf, letter, pdf.bounds.right - 200, pdf.bounds.bottom + 17, 180) + + render_destination_address( + pdf, + letter, + 150, + pdf.bounds.bottom + 210, + 300, + 100, + { size: 23, valign: :bottom, align: :left, font: self.class::ADDRESS_FONT } + ) + end + end + end +end diff --git a/app/lib/snail_mail/templates/hcb_stickers_template.rb b/app/lib/snail_mail/templates/hcb_stickers_template.rb new file mode 100644 index 0000000..6f38f39 --- /dev/null +++ b/app/lib/snail_mail/templates/hcb_stickers_template.rb @@ -0,0 +1,46 @@ +module SnailMail + module Templates + class HCBStickersTemplate < BaseTemplate + def self.template_name + "HCB Stickers" + end + + def render(pdf, letter) + pdf.image( + image_path("lilia-hcb-stickers-bg.png"), + at: [0, 288], + width: 432, + ) + + # Render speech bubble + # pdf.image( + # image_path(speech_bubble_image), + # at: [speech_position[:x], speech_position[:y]], + # width: speech_position[:width] + # ) + + # Render return address + render_return_address(pdf, letter, 10, 278, 146, 70) + + # Render destination address in speech bubble + render_destination_address( + pdf, + letter, + 192, + 149, + 226, + 57, + { size: 16, valign: :bottom, align: :left } + ) + + # Render IMb barcode + render_imb(pdf, letter, 216, 25, 207) + + render_letter_id(pdf, letter, 10, 12, 10) + render_qr_code(pdf, letter, 5, 196, 50) + + render_postage(pdf, letter) + end + end + end +end diff --git a/app/lib/snail_mail/templates/hcb_welcome_postcard_template.rb b/app/lib/snail_mail/templates/hcb_welcome_postcard_template.rb new file mode 100644 index 0000000..0780d2c --- /dev/null +++ b/app/lib/snail_mail/templates/hcb_welcome_postcard_template.rb @@ -0,0 +1,35 @@ +module SnailMail + module Templates + class HCBWelcomePostcardTemplate < HalfLetterTemplate + ADDRESS_FONT = "arial" + + def self.template_name + "hcb welcome postcard" + end + + SAMPLE_WELCOME_TEXT = "Hey! + + I'm super excited to work with your org because I think whatever you do is a really important cause and it aligns perfectly with our mission to support things that we believe are good. + + At HCB, we're all about empowering organizations like yours to make a real difference in the world. We believe in the power of community, innovation, and collaboration to create positive change. Your work resonates deeply with our values, and we can't wait to see the amazing things we'll accomplish together. + + We're here to support you every step of the way. Whether you need technical assistance, community resources, or just someone to bounce ideas off of, our team is ready to help. We're not just a service provider – we're your partner in making the world a better place. + + Let's build something incredible together! + + Warm regards, + The HCB Team" + + def render_front(pdf, letter) + pdf.bounding_box([10, pdf.bounds.top - 10], width: pdf.bounds.width - 20, height: pdf.bounds.height - 20) do + pdf.image(image_path("hcb/hcb-icon.png"), width: 60) + pdf.text_box("Welcome to HCB!", size: 30, at: [70, pdf.bounds.top - 18]) + end + + pdf.bounding_box([20, pdf.bounds.top - 90], width: pdf.bounds.width - 40, height: pdf.bounds.height - 100) do + pdf.text(letter.rubber_stamps || "", size: 15, align: :justify, overflow: :shrink_to_fit) + end + end + end + end +end diff --git a/app/lib/snail_mail/templates/hcpcxc_template.rb b/app/lib/snail_mail/templates/hcpcxc_template.rb new file mode 100644 index 0000000..0f5b6a5 --- /dev/null +++ b/app/lib/snail_mail/templates/hcpcxc_template.rb @@ -0,0 +1,55 @@ +module SnailMail + module Templates + class HcpcxcTemplate < BaseTemplate + def self.template_name + "hcpcxc" + end + + + def render(pdf, letter) + pdf.image( + image_path("dino-waving.png"), + at: [ 333, 163 ], + width: 87 + ) + + pdf.image( + image_path("hcpcxc_ra.png"), + at: [ 5, 288-5 ], + width: 175 + ) + + render_destination_address( + pdf, + letter, + 88, + 166, + 236, + 71, + { size: 16, valign: :bottom, align: :left } + ) + + # Render IMb barcode + render_imb(pdf, letter, 240, 24, 183) + + render_qr_code(pdf, letter, 5, 65, 60) + + render_letter_id(pdf, letter, 10, 19, 10) + if letter.rubber_stamps.present? + pdf.font("arial") do + pdf.text_box( + letter.rubber_stamps, + at: [ 294, 220 ], + width: 255, + height: 21, + overflow: :shrink_to_fit, + disable_wrap_by_char: true, + min_size: 1 + ) + end + end + render_postage(pdf, letter) + end + end + end +end diff --git a/app/lib/snail_mail/templates/heidi_readme_template.rb b/app/lib/snail_mail/templates/heidi_readme_template.rb new file mode 100644 index 0000000..d89f162 --- /dev/null +++ b/app/lib/snail_mail/templates/heidi_readme_template.rb @@ -0,0 +1,49 @@ +module SnailMail + module Templates + class HeidiReadmeTemplate < BaseTemplate + def self.template_name + "Heidi Can't Readme" + end + + def self.show_on_single? + true + end + + def render(pdf, letter) + render_return_address(pdf, letter, 10, 278, 190, 90, size: 12, font: "f25") + + render_destination_address( + pdf, + letter, + 133, + 176, + 256, + 107, + { size: 18, valign: :center, align: :left } + ) + + pdf.stroke do + pdf.line_width = 2.5 + pdf.rounded_rectangle([90 + 20, 189 - 5], 306, 122, 10) + end + + pdf.image( + image_path("msw-heidi-cant-readme.png"), + at: [6 + 20, 75], + width: 111, + ) + + pdf.image( + image_path("speech-tail.png"), + at: [114 + 20, 70 - 5], + width: 32.2, + ) + + render_imb(pdf, letter, 230, 25, 190) + render_letter_id(pdf, letter, 3, 15, 8, rotate: 90) + render_qr_code(pdf, letter, 7, 72 + 7 + 50 + 10 + 12, 60) + render_postage(pdf, letter) + end + end + end +end diff --git a/app/lib/snail_mail/templates/joyous_cat_template.rb b/app/lib/snail_mail/templates/joyous_cat_template.rb new file mode 100644 index 0000000..bc0f43b --- /dev/null +++ b/app/lib/snail_mail/templates/joyous_cat_template.rb @@ -0,0 +1,43 @@ +module SnailMail + module Templates + class JoyousCatTemplate < BaseTemplate + def self.template_name + "Joyous Cat :3" + end + + def self.show_on_single? + true + end + + def render(pdf, letter) + pdf.line_width = 3 + pdf.stroke do + pdf.rounded_rectangle([111, 189], 306, 122, 10) + end + + pdf.image( + image_path("acon-joyous-cat.png"), + at: [208, 74], + width: 106.4, + ) + + render_return_address(pdf, letter, 10, 270, 130, 70) + + render_destination_address( + pdf, + letter, + 134, + 173, + 266, + 67, + { size: 16, valign: :center, align: :left } + ) + + render_imb(pdf, letter, 131, 100, 266) + + render_qr_code(pdf, letter, 7, 72 + 7, 72) + render_postage(pdf, letter) + end + end + end +end diff --git a/app/lib/snail_mail/templates/kestrel_heidi_template.rb b/app/lib/snail_mail/templates/kestrel_heidi_template.rb new file mode 100644 index 0000000..147430d --- /dev/null +++ b/app/lib/snail_mail/templates/kestrel_heidi_template.rb @@ -0,0 +1,39 @@ +module SnailMail + module Templates + class KestrelHeidiTemplate < BaseTemplate + def self.template_name + "kestrel's heidi template!" + end + + def self.show_on_single? + true + end + + def render(pdf, letter) + pdf.image( + image_path("kestrel-mail-heidi.png"), + at: [107, 216], + width: 305, + ) + + render_return_address(pdf, letter, 10, 278, 190, 90, size: 14) + + render_destination_address( + pdf, + letter, + 126, + 201, + 266, + 67, + { size: 16, valign: :center, align: :left } + ) + + render_imb(pdf, letter, 124, 120, 200) + + render_qr_code(pdf, letter, 7, 72 + 7, 72) + + render_postage(pdf, letter) + end + end + end +end diff --git a/app/lib/snail_mail/templates/mail_orpheus_template.rb b/app/lib/snail_mail/templates/mail_orpheus_template.rb new file mode 100644 index 0000000..289bb46 --- /dev/null +++ b/app/lib/snail_mail/templates/mail_orpheus_template.rb @@ -0,0 +1,49 @@ +module SnailMail + module Templates + class MailOrpheusTemplate < BaseTemplate + def self.template_name + "Mail Orpheus!" + end + + def self.show_on_single? + true + end + + def render(pdf, letter) + pdf.image( + image_path("eleeza-mail-orpheus.png"), + at: [320, 113], + width: 106.4, + ) + + # Render speech bubble + # pdf.image( + # image_path(speech_bubble_image), + # at: [speech_position[:x], speech_position[:y]], + # width: speech_position[:width] + # ) + + # Render return address + render_return_address(pdf, letter, 10, 270, 130, 70) + + # Render destination address in speech bubble + render_destination_address( + pdf, + letter, + 79.5, + 202, + 237, + 100, + { size: 16, valign: :bottom, align: :left } + ) + + # Render IMb barcode + render_imb(pdf, letter, 78, 102, 237) + + # Render QR code for tracking + render_qr_code(pdf, letter, 7, 67, 60) + render_postage(pdf, letter) + end + end + end +end diff --git a/app/lib/snail_mail/templates/orpheus_template.rb b/app/lib/snail_mail/templates/orpheus_template.rb new file mode 100644 index 0000000..a4ff105 --- /dev/null +++ b/app/lib/snail_mail/templates/orpheus_template.rb @@ -0,0 +1,15 @@ +require_relative "character_template" + +module SnailMail + module Templates + class OrpheusTemplate < CharacterTemplate + def initialize(options = {}) + super(options.merge( + character_image: "eleeza-mail-orpheus.png", + character_position: { x: 10, y: 85, width: 130 }, + speech_position: { x: 95, y: 240, width: 290, height: 90 } + )) + end + end + end +end diff --git a/app/lib/snail_mail/templates/tarot_template.rb b/app/lib/snail_mail/templates/tarot_template.rb new file mode 100644 index 0000000..4ef835a --- /dev/null +++ b/app/lib/snail_mail/templates/tarot_template.rb @@ -0,0 +1,63 @@ +module SnailMail + module Templates + class TarotTemplate < BaseTemplate + def self.template_name + "Tarot" + end + + def render(pdf, letter) + render_return_address(pdf, letter, 10, 278, 190, 90, size: 12, font: 'comic') + + if letter.rubber_stamps.present? + pdf.font("gohu") do + pdf.text_box( + "\"#{letter.rubber_stamps}\"", + at: [ 137, 183 ], + width: 255, + height: 21, + overflow: :shrink_to_fit, + disable_wrap_by_char: true, + min_size: 1 + ) + end + end + + render_destination_address( + pdf, + letter, + 137, + 160, + 255, + 90, + { size: 16, valign: :center, align: :left } + ) + pdf.stroke do + pdf.line_width = 1 + pdf.line([ 137 - 25, 167 ], [ 392 + 25, 167 ]) + end + + pdf.stroke do + pdf.line_width = 2.5 + pdf.rounded_rectangle([ 111, 189 ], 306, 122, 10) + end + + pdf.image( + image_path("tarot/msw-joker.png"), + at: [ 6, 104 ], + width: 111 + ) + + pdf.image( + image_path("speech-tail.png"), + at: [ 118, 70 ], + width: 32.2 + ) + + render_imb(pdf, letter, 216, 25, 207) + render_letter_id(pdf, letter, 3, 15, 8, rotate: 90) + render_qr_code(pdf, letter, 7, 72 + 7, 72) + render_postage(pdf, letter) + end + end + end +end diff --git a/app/lib/test_print.pdf b/app/lib/test_print.pdf new file mode 100644 index 0000000..e9bc4f9 Binary files /dev/null and b/app/lib/test_print.pdf differ diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 0000000..77f5b4b --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: "team@hackclub.com" + layout "mailer" +end diff --git a/app/mailers/customs_receipt/receipt_mailer.rb b/app/mailers/customs_receipt/receipt_mailer.rb new file mode 100644 index 0000000..f4e19bd --- /dev/null +++ b/app/mailers/customs_receipt/receipt_mailer.rb @@ -0,0 +1,9 @@ +class CustomsReceipt::ReceiptMailer < ApplicationMailer + def receipt + @order_number = params[:order_number] + @pdf_data = params[:pdf_data] + @recipient = params[:email] + + mail to: @recipient + end +end diff --git a/app/mailers/generic_text_mailer.rb b/app/mailers/generic_text_mailer.rb new file mode 100644 index 0000000..e2de0ca --- /dev/null +++ b/app/mailers/generic_text_mailer.rb @@ -0,0 +1,3 @@ +class GenericTextMailer < LoopsMailer + layout "text_mailer" +end diff --git a/app/mailers/loops_mailer.rb b/app/mailers/loops_mailer.rb new file mode 100644 index 0000000..f824e74 --- /dev/null +++ b/app/mailers/loops_mailer.rb @@ -0,0 +1,2 @@ +class LoopsMailer < ApplicationMailer +end diff --git a/app/mailers/public/login_code_mailer.rb b/app/mailers/public/login_code_mailer.rb new file mode 100644 index 0000000..96783e5 --- /dev/null +++ b/app/mailers/public/login_code_mailer.rb @@ -0,0 +1,10 @@ +# this is a loops mailer until they unfreeze our account +class Public::LoginCodeMailer < GenericTextMailer + def send_login_code(email, login_code) + @subject = "(hack club) here's your mail login link!" + @recipient = email + @login_code_url = login_code_url login_code + + mail to: @recipient + end +end diff --git a/app/mailers/usps/payment_account_mailer.rb b/app/mailers/usps/payment_account_mailer.rb new file mode 100644 index 0000000..fb376dd --- /dev/null +++ b/app/mailers/usps/payment_account_mailer.rb @@ -0,0 +1,10 @@ +class USPS::PaymentAccountMailer < GenericTextMailer + def get_your_eps_racks_up(accounts:) + @count = accounts.length + @subject = "[theseus] [usps] #{@count} EPS #{"account".pluralize(@count)} #{"is".pluralize(@count)} broke" + @recipient = "nora@hackclub.com" + @accounts = accounts + + mail to: "dinobox@hackclub.com" + end +end diff --git a/app/mailers/warehouse/order_mailer.rb b/app/mailers/warehouse/order_mailer.rb new file mode 100644 index 0000000..b339bbc --- /dev/null +++ b/app/mailers/warehouse/order_mailer.rb @@ -0,0 +1,15 @@ +class Warehouse::OrderMailer < ApplicationMailer + def order_created + @order = params[:order] + @recipient = @order.recipient_email + + mail to: @recipient + end + + def order_shipped + @order = params[:order] + @recipient = @order.recipient_email + + mail to: @recipient + end +end diff --git a/app/models/address.rb b/app/models/address.rb new file mode 100644 index 0000000..685d91a --- /dev/null +++ b/app/models/address.rb @@ -0,0 +1,86 @@ +# == Schema Information +# +# Table name: addresses +# +# id :bigint not null, primary key +# city :string +# country :integer +# email :string +# first_name :string +# last_name :string +# line_1 :string +# line_2 :string +# phone_number :string +# postal_code :string +# state :string +# created_at :datetime not null +# updated_at :datetime not null +# batch_id :bigint +# +# Indexes +# +# index_addresses_on_batch_id (batch_id) +# +# Foreign Keys +# +# fk_rails_... (batch_id => batches.id) +# +class Address < ApplicationRecord + include CountryEnumable + has_country_enum + + GREMLINS = [ + "\u200E", # LEFT-TO-RIGHT MARK + "\u200B", # ZERO WIDTH SPACE + ].join + + def self.strip_gremlins(str) + str&.delete(GREMLINS)&.presence + end + + validates_presence_of :first_name, :line_1, :city, :state, :postal_code, :country + + before_validation :strip_gremlins_from_fields + + def name_line + [first_name, last_name].join(" ") + end + + def us_format + <<~EOA + #{name_line} + #{[line_1, line_2].compact_blank.join("\n")} + #{city}, #{state} #{postal_code} + #{country} + EOA + end + + def us? + country == "US" + end + + def snailify(origin = "US") + SnailButNbsp.new( + name: name_line, + line_1:, + line_2: line_2.presence, + city:, + region: state, + postal_code:, + country: country, + origin: origin, + ).to_s + end + + private + + def strip_gremlins_from_fields + self.first_name = Address.strip_gremlins(first_name) + self.last_name = Address.strip_gremlins(last_name) + self.line_1 = Address.strip_gremlins(line_1) + self.line_2 = Address.strip_gremlins(line_2) + self.city = Address.strip_gremlins(city) + self.state = Address.strip_gremlins(state) + self.postal_code = Address.strip_gremlins(postal_code) + end +end diff --git a/app/models/airtable/warehouse_sku.rb b/app/models/airtable/warehouse_sku.rb new file mode 100644 index 0000000..2153955 --- /dev/null +++ b/app/models/airtable/warehouse_sku.rb @@ -0,0 +1,24 @@ +class Airtable::WarehouseSKU < Norairrecord::Table + self.base_key = "appK53aN0fz3sgJ4w" + self.table_name = "tblvSJMqoXnQyN7co" + + def sku + fields["SKU"] + end + + def name + fields["Name (Must Match Poster Requests)"] + end + + def in_stock + fields["In Stock"] + end + + def unit_cost + fields["Unit Cost"] + end + + def item_type + fields["Item Type"] + end +end diff --git a/app/models/api_key.rb b/app/models/api_key.rb new file mode 100644 index 0000000..691e07b --- /dev/null +++ b/app/models/api_key.rb @@ -0,0 +1,65 @@ +# == Schema Information +# +# Table name: api_keys +# +# id :bigint not null, primary key +# may_impersonate :boolean +# name :string +# pii :boolean +# revoked_at :datetime +# token_bidx :string +# token_ciphertext :text +# created_at :datetime not null +# updated_at :datetime not null +# user_id :bigint not null +# +# Indexes +# +# index_api_keys_on_token_bidx (token_bidx) UNIQUE +# index_api_keys_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (user_id => users.id) +# +class APIKey < ApplicationRecord + belongs_to :user + + validates :token, presence: true, uniqueness: true + + scope :not_revoked, -> { where(revoked_at: nil).or(where(revoked_at: Time.now..)) } + scope :accessible, -> { not_revoked } + + before_validation :generate_token, on: :create + + TOKEN = ExternalToken.new("api") + + has_encrypted :token + blind_index :token + + def pretty_name + "#{user.username}@#{name}" + end + + def revoke! + update!(revoked_at: Time.now) + end + + def revoked? + revoked_at.present? + end + + def active? + !revoked? + end + + def abbreviated + "#{token[..15]}.....#{token[-5..]}" + end + + private + + def generate_token + self.token ||= TOKEN.generate + end +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 0000000..b63caeb --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/app/models/batch.rb b/app/models/batch.rb new file mode 100644 index 0000000..dee3556 --- /dev/null +++ b/app/models/batch.rb @@ -0,0 +1,238 @@ +# == Schema Information +# +# Table name: batches +# +# id :bigint not null, primary key +# aasm_state :string +# address_count :integer +# field_mapping :jsonb +# letter_height :decimal(, ) +# letter_mailing_date :date +# letter_processing_category :integer +# letter_return_address_name :string +# letter_weight :decimal(, ) +# letter_width :decimal(, ) +# tags :citext default([]), is an Array +# template_cycle :string default([]), is an Array +# type :string not null +# warehouse_user_facing_title :string +# created_at :datetime not null +# updated_at :datetime not null +# letter_mailer_id_id :bigint +# letter_queue_id :bigint +# letter_return_address_id :bigint +# user_id :bigint not null +# warehouse_template_id :bigint +# +# Indexes +# +# index_batches_on_letter_mailer_id_id (letter_mailer_id_id) +# index_batches_on_letter_queue_id (letter_queue_id) +# index_batches_on_letter_return_address_id (letter_return_address_id) +# index_batches_on_tags (tags) USING gin +# index_batches_on_type (type) +# index_batches_on_user_id (user_id) +# index_batches_on_warehouse_template_id (warehouse_template_id) +# +# Foreign Keys +# +# fk_rails_... (letter_mailer_id_id => usps_mailer_ids.id) +# fk_rails_... (letter_queue_id => letter_queues.id) +# fk_rails_... (letter_return_address_id => return_addresses.id) +# fk_rails_... (user_id => users.id) +# fk_rails_... (warehouse_template_id => warehouse_templates.id) +# +class Batch < ApplicationRecord + include AASM + include PublicIdentifiable + set_public_id_prefix "batch" + + include Taggable + + aasm timestamps: true do + state :awaiting_field_mapping, initial: true + state :fields_mapped + state :processed + + event :mark_fields_mapped do + transitions from: :awaiting_field_mapping, to: :fields_mapped + end + + event :mark_processed do + transitions from: :fields_mapped, to: :processed + after do + User::UpdateTasksJob.perform_now(user) + end + end + end + + self.inheritance_column = "type" + belongs_to :user + belongs_to :letter_queue, optional: true, class_name: "Letter::Queue" + has_one_attached :csv + has_one_attached :labels_pdf + has_one_attached :pdf_document + has_many :addresses, dependent: :destroy + + after_save :update_associated_tags, if: :saved_change_to_tags? + + def origin + if letter_queue.present? + "queue: #{letter_queue.name}" + elsif csv.present? + "csv: #{csv.filename}" + else + "unknown" + end + end + + def csv_data + csv.open do |file| + File.read(file, encoding: "bom|utf-8") + end + end + + def attach_pdf(pdf_data) + PdfAttachmentUtil.attach_pdf(pdf_data, self, :pdf_document) + end + + def total_cost + raise NotImplementedError, "Subclasses must implement total_cost" + end + + GREMLINS = [ + "‎", + "​", + ].join + + def run_map! + rows = CSV.parse(csv_data, headers: true, converters: [->(s) { s&.strip&.delete(GREMLINS).presence }]) + + # Phase 1: Collect all address data + address_attributes = [] + row_map = {} # Keep rows in a hash + Parallel.each(rows.each_with_index, in_threads: 8) do |row, i| + begin + # Skip rows where first_name is blank + next if row[field_mapping["first_name"]].blank? + + address_attrs = build_address_attributes(row) + if address_attrs + address_attributes << address_attrs + row_map[i] = row # Store row in hash + end + rescue => e + Rails.logger.error("Error processing row #{i} in batch #{id}: #{e.message}") + raise + end + end + + # Bulk insert all addresses + if address_attributes.any? + now = Time.current + address_attributes.each do |attrs| + attrs[:created_at] = now + attrs[:updated_at] = now + attrs[:batch_id] = id + end + + begin + Address.insert_all!(address_attributes) + rescue ActiveRecord::RecordInvalid => e + Rails.logger.error("Failed to insert addresses: #{e.message}") + raise + end + + # Phase 2: Create associated records (letters) for each address + # Fetch all addresses we just created + addresses = Address.where(batch_id: id).where(created_at: now).to_a + + Parallel.each(addresses.each_with_index, in_threads: 8) do |address, i| + begin + ActiveRecord::Base.connection_pool.with_connection do + ActiveRecord::Base.transaction do + build_mapping(row_map[i], address) + end + end + rescue => e + Rails.logger.error("Error creating associated records for address #{address.id} in batch #{id}: #{e.message}") + raise + end + end + end + + mark_fields_mapped + save! + end + + private + + def build_address_attributes(row) + csv_country = row[field_mapping["country"]] + country = FrickinCountryNames.find_country(csv_country) + postal_code = row[field_mapping["postal_code"]] + state = row[field_mapping["state"]] + + # Try AI translation if: + # 1. Country couldn't be found by FrickinCountryNames, or + # 2. Country is not US + if country.nil? || country.alpha2 != "US" + begin + translated = AIService.fix_address(row, field_mapping) + if translated + # If AI translation succeeded, try to find the country again + translated_country = FrickinCountryNames.find_country(translated[:country]) + if translated_country + # Preserve original first_name and last_name + translated[:first_name] = row[field_mapping["first_name"]] + translated[:last_name] = row[field_mapping["last_name"]] + translated[:country] = translated_country.alpha2 + return translated + end + end + rescue => e + Rails.logger.error("AI translation failed for batch #{id}: #{e.message}") + raise + end + end + + # Process US addresses or fallback for failed translations + if country&.alpha2 == "US" && postal_code.present? && postal_code.length < 5 + postal_code = postal_code.rjust(5, "0") + end + + # Normalize state name to abbreviation if country is found + normalized_state = if country + FrickinCountryNames.normalize_state(country, state) + else + state + end + + { + first_name: row[field_mapping["first_name"]], + last_name: row[field_mapping["last_name"]], + line_1: row[field_mapping["line_1"]], + line_2: row[field_mapping["line_2"]], + city: row[field_mapping["city"]], + state: normalized_state, + postal_code: postal_code, + country: country&.alpha2 || csv_country&.upcase, # Use FCN alpha2 if available, otherwise original country code + phone_number: row[field_mapping["phone_number"]], + email: row[field_mapping["email"]], + } + end + + def build_mapping(row, address) + # Base class just returns the address + address + end + + def update_associated_tags + case type + when "Letter::Batch" + Letter.where(batch_id: id).update_all(tags: tags) + when "Warehouse::Batch" + Warehouse::Order.where(batch_id: id).update_all(tags: tags) + end + end +end diff --git a/app/models/common_tag.rb b/app/models/common_tag.rb new file mode 100644 index 0000000..2f2e62a --- /dev/null +++ b/app/models/common_tag.rb @@ -0,0 +1,12 @@ +# == Schema Information +# +# Table name: common_tags +# +# id :bigint not null, primary key +# implies_ysws :boolean +# tag :string +# created_at :datetime not null +# updated_at :datetime not null +# +class CommonTag < ApplicationRecord +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/models/concerns/can_be_batched.rb b/app/models/concerns/can_be_batched.rb new file mode 100644 index 0000000..50c6347 --- /dev/null +++ b/app/models/concerns/can_be_batched.rb @@ -0,0 +1,10 @@ +module CanBeBatched + extend ActiveSupport::Concern + + included do + belongs_to :batch, optional: true + + scope :in_batch, -> { where.not(batch_id: nil) } + scope :not_in_batch, -> { where(batch_id: nil) } + end +end diff --git a/app/models/concerns/country_enumable.rb b/app/models/concerns/country_enumable.rb new file mode 100644 index 0000000..5966fd2 --- /dev/null +++ b/app/models/concerns/country_enumable.rb @@ -0,0 +1,276 @@ +# frozen_string_literal: true + +module CountryEnumable + extend ActiveSupport::Concern + + included do + def self.countries_for_select + countries = self.countries.keys.map do |alpha2| + [ alpha2, ISO3166::Country[alpha2].common_name ] + end.sort_by { |c| I18n.transliterate(c.last) } + countries.unshift([ "US", "United States" ], [ "CA", "Canada" ]).uniq! + end + end + + class_methods do + def has_country_enum(field: :country) + enum field, self.country_enum_list, prefix: field + end + + private + + def country_enum_list + { + AD: 6, + AE: 235, + AF: 1, + AG: 10, + AI: 8, + AL: 3, + AM: 12, + AO: 7, + AQ: 9, + AR: 11, + AS: 5, + AT: 15, + AU: 14, + AW: 13, + AX: 2, + AZ: 16, + BA: 29, + BB: 20, + BD: 19, + BE: 22, + BF: 36, + BG: 35, + BH: 18, + BI: 37, + BJ: 24, + BL: 186, + BM: 25, + BN: 34, + BO: 27, + BQ: 28, + BR: 32, + BS: 17, + BT: 26, + BV: 31, + BW: 30, + BY: 21, + BZ: 23, + CA: 41, + CC: 48, + CD: 52, + CF: 43, + CG: 51, + CH: 217, + CI: 55, + CK: 53, + CL: 45, + CM: 40, + CN: 46, + CO: 49, + CR: 54, + CU: 57, + CV: 38, + CW: 58, + CX: 47, + CY: 59, + CZ: 60, + DE: 84, + DJ: 62, + DK: 61, + DM: 63, + DO: 64, + DZ: 4, + EC: 65, + EE: 70, + EG: 66, + EH: 246, + ER: 69, + ES: 210, + ET: 72, + FI: 76, + FJ: 75, + FK: 73, + FM: 145, + FO: 74, + FR: 77, + GA: 81, + GB: 236, + GD: 89, + GE: 83, + GF: 78, + GG: 93, + GH: 85, + GI: 86, + GL: 88, + GM: 82, + GN: 94, + GP: 90, + GQ: 68, + GR: 87, + GS: 208, + GT: 92, + GU: 91, + GW: 95, + GY: 96, + HK: 101, + HM: 98, + HN: 100, + HR: 56, + HT: 97, + HU: 102, + ID: 105, + IE: 108, + IL: 110, + IM: 109, + IN: 104, + IO: 33, + IQ: 107, + IR: 106, + IS: 103, + IT: 111, + JE: 114, + JM: 112, + JO: 115, + JP: 113, + KE: 117, + KG: 122, + KH: 39, + KI: 118, + KM: 50, + KN: 188, + KP: 119, + KR: 120, + KW: 121, + KY: 42, + KZ: 116, + LA: 123, + LB: 125, + LC: 189, + LI: 129, + LK: 211, + LR: 127, + LS: 126, + LT: 130, + LU: 131, + LV: 124, + LY: 128, + MA: 151, + MC: 147, + MD: 146, + ME: 149, + MF: 190, + MG: 133, + MH: 139, + MK: 165, + ML: 137, + MM: 153, + MN: 148, + MO: 132, + MP: 166, + MQ: 140, + MR: 141, + MS: 150, + MT: 138, + MU: 142, + MV: 136, + MW: 134, + MX: 144, + MY: 135, + MZ: 152, + NA: 154, + NC: 158, + NE: 161, + NF: 164, + NG: 162, + NI: 160, + NL: 157, + NO: 167, + NP: 156, + NR: 155, + NU: 163, + NZ: 159, + OM: 168, + PA: 172, + PE: 175, + PF: 79, + PG: 173, + PH: 176, + PK: 169, + PL: 178, + PM: 191, + PN: 177, + PR: 180, + PS: 171, + PT: 179, + PW: 170, + PY: 174, + QA: 181, + RE: 182, + RO: 183, + RS: 198, + RU: 184, + RW: 185, + SA: 196, + SB: 205, + SC: 199, + SD: 212, + SE: 216, + SG: 201, + SH: 187, + SI: 204, + SJ: 214, + SK: 203, + SL: 200, + SM: 194, + SN: 197, + SO: 206, + SR: 213, + SS: 209, + ST: 195, + SV: 67, + SX: 202, + SY: 218, + SZ: 71, + TC: 231, + TD: 44, + TF: 80, + TG: 224, + TH: 222, + TJ: 220, + TK: 225, + TL: 223, + TM: 230, + TN: 228, + TO: 226, + TR: 229, + TT: 227, + TV: 232, + TW: 219, + TZ: 221, + UA: 234, + UG: 233, + UM: 237, + US: 215, + UY: 238, + UZ: 239, + VA: 99, + VC: 192, + VE: 241, + VG: 243, + VI: 244, + VN: 242, + VU: 240, + WF: 245, + WS: 193, + YE: 247, + YT: 143, + ZA: 207, + ZM: 248, + ZW: 249 + } + end + end +end diff --git a/app/models/concerns/has_address.rb b/app/models/concerns/has_address.rb new file mode 100644 index 0000000..4c4078a --- /dev/null +++ b/app/models/concerns/has_address.rb @@ -0,0 +1,8 @@ +module HasAddress + extend ActiveSupport::Concern + + included do + belongs_to :address + accepts_nested_attributes_for :address, update_only: true + end +end diff --git a/app/models/concerns/has_table_sync.rb b/app/models/concerns/has_table_sync.rb new file mode 100644 index 0000000..4486d07 --- /dev/null +++ b/app/models/concerns/has_table_sync.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module HasTableSync + RECORD_LIMIT_PER_CALL = 10_000 + + extend ActiveSupport::Concern + included do + def self.has_table_sync(base, table, mapping, scope: nil) + @table_sync_base = base + @table_sync_table = table + @table_sync_mapping = mapping + @table_sync_scope = scope + + self.class.define_method(:mirror_to_airtable!) do |sync_id| + headers = @table_sync_mapping.keys.map(&:to_s) + records = (@table_sync_scope ? self.send(@table_sync_scope) : self.all).load + + records.each_slice(RECORD_LIMIT_PER_CALL) do |chunk| + csv = CSV.generate do |csv| + csv << headers + chunk.each do |record| + row = [] + @table_sync_mapping.values.each do |field| + row << (field.class == Symbol ? record.try(field) : record.instance_eval(&field)) + end + csv << row + end + end + url = "#{ENV["AIRTABLE_BASE_URL"] || "https://api.airtable.com"}/v0/#{@table_sync_base}/#{@table_sync_table}/sync/#{sync_id}" + res = JSON.parse(HTTP.post( + url, + headers: { "Authorization" => "Bearer #{Rails.application.credentials.dig(:airtable, :pat)}", "Content-Type" => "text/csv" }, + body: csv + )) + raise StandardError, res["error"] if res["error"] + end + nil + end + end + end +end diff --git a/app/models/concerns/has_warehouse_line_items.rb b/app/models/concerns/has_warehouse_line_items.rb new file mode 100644 index 0000000..b645137 --- /dev/null +++ b/app/models/concerns/has_warehouse_line_items.rb @@ -0,0 +1,24 @@ +module HasWarehouseLineItems + extend ActiveSupport::Concern + + included do + has_many :line_items, dependent: :destroy + accepts_nested_attributes_for :line_items, reject_if: :all_blank, allow_destroy: true + has_many :skus, through: :line_items + end + + def labor_cost + # $1.80 base * 20¢/SKU + 1.80 + (0.20 * skus.distinct.count) + end + + def contents_actual_cost_to_hc + line_items.joins(:sku).sum("warehouse_skus.actual_cost_to_hc * warehouse_line_items.quantity") + end + + def contents_declared_unit_cost + line_items.includes(:sku).sum do |line_item| + (line_item.sku.declared_unit_cost || 0) * line_item.quantity + end + end +end diff --git a/app/models/concerns/has_zenventory_url.rb b/app/models/concerns/has_zenventory_url.rb new file mode 100644 index 0000000..eb73371 --- /dev/null +++ b/app/models/concerns/has_zenventory_url.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module HasZenventoryUrl + extend ActiveSupport::Concern + included do + def self.has_zenventory_url(format, id_field = :zenventory_id) + self.define_method(:zenventory_url) do + id = self.try(id_field) + return if id.nil? + + format % id + end + end + end +end diff --git a/app/models/concerns/public_identifiable.rb b/app/models/concerns/public_identifiable.rb new file mode 100644 index 0000000..0bd4262 --- /dev/null +++ b/app/models/concerns/public_identifiable.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +# (@msw) Stripe-like public IDs that don't require adding a column to the database. +module PublicIdentifiable + extend ActiveSupport::Concern + + included do + include Hashid::Rails + class_attribute :public_id_prefix + end + + def public_id + "#{self.public_id_prefix}!#{hashid}" + end + + module ClassMethods + def set_public_id_prefix(prefix) + self.public_id_prefix = prefix.to_s.downcase + end + + def find_by_public_id(id) + return nil unless id.is_a? String + + prefix = id.split("!").first.to_s.downcase + hash = id.split("!").last + return nil unless prefix == self.get_public_id_prefix + + # ex. 'org_h1izp' + find_by_hashid(hash) + end + + def find_by_public_id!(id) + obj = find_by_public_id id + raise ActiveRecord::RecordNotFound.new(nil, self.name) if obj.nil? + + obj + end + + def get_public_id_prefix + return self.public_id_prefix.to_s.downcase if self.public_id_prefix.present? + + raise NotImplementedError, "The #{self.class.name} model includes PublicIdentifiable module, but set_public_id_prefix hasn't been called." + end + end +end diff --git a/app/models/concerns/taggable.rb b/app/models/concerns/taggable.rb new file mode 100644 index 0000000..d9968b4 --- /dev/null +++ b/app/models/concerns/taggable.rb @@ -0,0 +1,19 @@ +module Taggable + extend ActiveSupport::Concern + + included do + taggable_array :tags + before_save :zap_empty_tags + after_save :update_tag_cache, if: :saved_change_to_tags? + end + + def zap_empty_tags + tags.reject!(&:blank?) + end + + private + + def update_tag_cache + UpdateTagCacheJob.perform_later + end +end \ No newline at end of file diff --git a/app/models/letter.rb b/app/models/letter.rb new file mode 100644 index 0000000..3397207 --- /dev/null +++ b/app/models/letter.rb @@ -0,0 +1,310 @@ +# == Schema Information +# +# Table name: letters +# +# id :bigint not null, primary key +# aasm_state :string +# body :text +# height :decimal(, ) +# idempotency_key :string +# imb_rollover_count :integer +# imb_serial_number :integer +# mailed_at :datetime +# mailing_date :date +# metadata :jsonb +# non_machinable :boolean +# postage :decimal(, ) +# postage_type :integer +# printed_at :datetime +# processing_category :integer +# received_at :datetime +# recipient_email :string +# return_address_name :string +# rubber_stamps :text +# tags :citext default([]), is an Array +# user_facing_title :string +# weight :decimal(, ) +# width :decimal(, ) +# created_at :datetime not null +# updated_at :datetime not null +# address_id :bigint not null +# batch_id :bigint +# letter_queue_id :bigint +# return_address_id :bigint not null +# user_id :bigint not null +# usps_mailer_id_id :bigint not null +# +# Indexes +# +# index_letters_on_address_id (address_id) +# index_letters_on_batch_id (batch_id) +# index_letters_on_idempotency_key (idempotency_key) UNIQUE +# index_letters_on_imb_serial_number (imb_serial_number) +# index_letters_on_letter_queue_id (letter_queue_id) +# index_letters_on_return_address_id (return_address_id) +# index_letters_on_tags (tags) USING gin +# index_letters_on_user_id (user_id) +# index_letters_on_usps_mailer_id_id (usps_mailer_id_id) +# +# Foreign Keys +# +# fk_rails_... (address_id => addresses.id) +# fk_rails_... (batch_id => batches.id) +# fk_rails_... (letter_queue_id => letter_queues.id) +# fk_rails_... (return_address_id => return_addresses.id) +# fk_rails_... (user_id => users.id) +# fk_rails_... (usps_mailer_id_id => usps_mailer_ids.id) +# +class Letter < ApplicationRecord + include PublicIdentifiable + set_public_id_prefix "ltr" + + include HasAddress + include CanBeBatched + include AASM + include Taggable + # Add ActiveStorage attachment for the label PDF + has_one_attached :label + belongs_to :return_address, optional: true + has_many :iv_mtr_events, class_name: "USPS::IVMTR::Event" + belongs_to :user + belongs_to :queue, class_name: "Letter::Queue", foreign_key: "letter_queue_id", optional: true + + aasm timestamps: true do + state :queued + state :pending, initial: true + state :printed + state :mailed + state :received + + event :batch_from_queue do + transitions from: :queued, to: :pending + end + + event :mark_printed do + transitions from: :pending, to: :printed + end + + event :mark_mailed do + transitions from: [:pending, :printed], to: :mailed + end + + event :mark_received do + transitions from: :mailed, to: :received + end + + event :unreceive do + transitions from: :received, to: :mailed + end + end + + def display_name + user_facing_title || tags.compact_blank.join(", ") || public_id + end + + def return_address_name_line + return_address_name.presence || return_address&.name + end + + def been_mailed? + mailed? || received? + end + + belongs_to :usps_mailer_id, class_name: "USPS::MailerId" + + after_create :set_imb_sequence + + # Generate a label for this letter + def generate_label(options = {}) + pdf = SnailMail::Service.generate_label(self, options) + + # Directly attach the PDF to this letter + attach_pdf(pdf.render) + + # Save the record to persist the attachment + save + end + + # Directly attach a PDF to this letter + def attach_pdf(pdf_data) + io = StringIO.new(pdf_data) + + label.attach( + io: io, + filename: "label_#{Time.now.to_i}.pdf", + content_type: "application/pdf", + ) + end + + def flirt + desired_price = USPS::PricingEngine.fcmi_price( + processing_category, + weight, + address.country + ) + USPS::FLIRTEngine.closest_us_price(desired_price) + end + + def self.find_by_imb_sn(imb_sn, mailer_id = nil) + query = where(imb_serial_number: imb_sn.to_i) + query = query.where(usps_mailer_id: mailer_id) if mailer_id + query.order(imb_rollover_count: :desc).first + end + + enum :processing_category, { + letter: 0, + flat: 1, + }, instance_methods: false, prefix: true, suffix: true + + enum :postage_type, { + stamps: 0, + indicia: 1, + international_origin: 2, + }, instance_methods: false + + has_one :usps_indicium, class_name: "USPS::Indicium" + + attribute :mailing_date, :date + validates :mailing_date, presence: true, if: -> { postage_type == "indicia" } + validate :mailing_date_not_in_past, if: -> { mailing_date.present? }, on: :create + validates :processing_category, presence: true + validate :validate_postage_type_by_return_address + + before_save :set_postage + + def mailing_date_not_in_past + if mailing_date < Date.current + errors.add(:mailing_date, "cannot be in the past") + end + end + + def validate_postage_type_by_return_address + if return_address.present? && postage_type.present? + if return_address.us? + if postage_type == "international_origin" + errors.add(:postage_type, "cannot be international origin when return address is in the US") + end + else + if postage_type != "international_origin" + errors.add(:postage_type, "must be international origin when return address is not in the US") + end + end + end + end + + def default_mailing_date + now = Time.current + today = now.to_date + + # If it's before 4PM on a business day, default to today + if now.hour < 16 && today.on_weekday? + today + else + # Otherwise, default to next business day + next_business_day = today + loop do + next_business_day += 1 + break if next_business_day.on_weekday? + end + next_business_day + end + end + + def to_param + self.public_id + end + + def events + iv = iv_mtr_events.map do |event| + e = event.hydrated + { + happened_at: event.happened_at.in_time_zone("America/New_York"), + source: "USPS IV-MTR", + location: "#{e.scan_facility_city}, #{e.scan_facility_state} #{e.scan_facility_zip}", + facility: "#{e.scan_facility_name} (#{e.scan_locale_key})", + description: "[OP#{e.opcode.code}] #{e.opcode.process_description}", + extra_info: "#{e.handling_event_type_description} – #{e.mail_phase} – #{e.machine_name} (#{event.payload.dig("machineId") || "no ID"})", + } + end + timestamps = [] + location = return_address.location + timestamps << { + happened_at: printed_at.in_time_zone("America/New_York"), + source: "Hack Club", + facility: "Mailer", + description: "Letter printed.", + location:, + } if printed_at + timestamps << { + happened_at: mailed_at.in_time_zone("America/New_York"), + source: "Hack Club", + facility: "Mailer", + description: "Letter mailed!", + location:, + } if mailed_at + timestamps << { + happened_at: received_at.in_time_zone("America/New_York"), + source: "You!", + facility: "Your mailbox", + description: "You received this letter!", + location: "wherever you live", + } if received_at + (iv + timestamps).sort_by { |event| event[:happened_at] } + end + + private + + def set_postage + self.postage = case postage_type + when "indicia" + if usps_indicium.present? + # Use actual indicia price if indicia are bought + usps_indicium.cost + elsif address.us? + # For US mail without bought indicia, use metered price + USPS::PricingEngine.metered_price( + processing_category, + weight, + non_machinable + ) + else + # For international mail without bought indicia, use FLIRT-ed price + flirted = flirt + USPS::PricingEngine.metered_price( + flirted[:processing_category], + flirted[:weight], + flirted[:non_machinable] + ) + end + when "stamps" + if %i(queued pending).include?(aasm.current_state) + return 0 + end + # For stamps, use stamp price for US and desired price for international + if address.us? + USPS::PricingEngine.domestic_stamp_price( + processing_category, + weight, + non_machinable + ) + else + USPS::PricingEngine.fcmi_price( + processing_category, + weight, + address.country, + non_machinable + ) + end + when "international_origin" + 0 + end + end + + def set_imb_sequence + sn, rollover = usps_mailer_id.next_sn_and_rollover + update_columns( + imb_serial_number: sn, + imb_rollover_count: rollover, + ) + end +end diff --git a/app/models/letter/batch.rb b/app/models/letter/batch.rb new file mode 100644 index 0000000..ff15678 --- /dev/null +++ b/app/models/letter/batch.rb @@ -0,0 +1,362 @@ +# == Schema Information +# +# Table name: batches +# +# id :bigint not null, primary key +# aasm_state :string +# address_count :integer +# field_mapping :jsonb +# letter_height :decimal(, ) +# letter_mailing_date :date +# letter_processing_category :integer +# letter_return_address_name :string +# letter_weight :decimal(, ) +# letter_width :decimal(, ) +# tags :citext default([]), is an Array +# template_cycle :string default([]), is an Array +# type :string not null +# warehouse_user_facing_title :string +# created_at :datetime not null +# updated_at :datetime not null +# letter_mailer_id_id :bigint +# letter_queue_id :bigint +# letter_return_address_id :bigint +# user_id :bigint not null +# warehouse_template_id :bigint +# +# Indexes +# +# index_batches_on_letter_mailer_id_id (letter_mailer_id_id) +# index_batches_on_letter_queue_id (letter_queue_id) +# index_batches_on_letter_return_address_id (letter_return_address_id) +# index_batches_on_tags (tags) USING gin +# index_batches_on_type (type) +# index_batches_on_user_id (user_id) +# index_batches_on_warehouse_template_id (warehouse_template_id) +# +# Foreign Keys +# +# fk_rails_... (letter_mailer_id_id => usps_mailer_ids.id) +# fk_rails_... (letter_queue_id => letter_queues.id) +# fk_rails_... (letter_return_address_id => return_addresses.id) +# fk_rails_... (user_id => users.id) +# fk_rails_... (warehouse_template_id => warehouse_templates.id) +# +class Letter::Batch < Batch + self.inheritance_column = "type" + # default_scope { where(type: 'letters') } + has_many :letters, dependent: :destroy + belongs_to :mailer_id, class_name: "USPS::MailerId", foreign_key: "letter_mailer_id_id", optional: true + belongs_to :letter_return_address, class_name: "ReturnAddress", optional: true + belongs_to :letter_queue, :class_name => "Letter::Queue", optional: true + + # Add ActiveStorage attachment for the batch label PDF + has_one_attached :pdf_label + + # Add batch-level letter specifications + attribute :letter_height, :decimal + attribute :letter_width, :decimal + attribute :letter_weight, :decimal + attribute :letter_processing_category, :integer + attribute :user_facing_title, :string + attribute :letter_return_address_name, :string + attribute :letter_queue_id, :integer + attr_accessor :template, :template_cycle + attribute :letter_mailing_date, :date + + validates :letter_height, :letter_width, :letter_weight, presence: true, numericality: { greater_than: 0 } + validates :mailer_id, presence: true + validates :letter_return_address, presence: true, on: :process + validates :letter_mailing_date, presence: true, on: :process + validate :mailing_date_not_in_past, if: -> { letter_mailing_date.present? }, on: :create + validates :letter_processing_category, presence: true + + after_update :update_letter_tags, if: :saved_change_to_tags? + + def self.model_name + Batch.model_name + end + + # Directly attach a PDF to this batch + def attach_pdf(pdf_data) + io = StringIO.new(pdf_data) + + pdf_label.attach( + io: io, + filename: "label_batch_#{Time.now.to_i}.pdf", + content_type: "application/pdf", + ) + end + + def process!(options = {}) + return false unless fields_mapped? + + # Set postage types and user_facing_title for all letters based on options + if options[:us_postage_type].present? || options[:intl_postage_type].present? || options[:user_facing_title].present? + letters.each do |letter| + letter.mailing_date = letter_mailing_date + if letter.return_address.us? + # For US return addresses, use the US postage type + letter.postage_type = options[:us_postage_type] + else + # For non-US return addresses, must use international origin + letter.postage_type = "international_origin" + end + letter.user_facing_title = options[:user_facing_title] if options[:user_facing_title].present? + letter.save! + end + end + + # Purchase indicia for all letters if needed + if options[:payment_account].present? && + (options[:us_postage_type] == "indicia" || options[:intl_postage_type] == "indicia") + # Check if there are sufficient funds before processing + indicia_cost = letters.includes(:address).sum do |letter| + if letter.postage_type == "indicia" + if letter.address.us? + USPS::PricingEngine.metered_price( + letter.processing_category, + letter.weight, + letter.non_machinable + ) + else + flirted = letter.flirt + USPS::PricingEngine.metered_price( + flirted[:processing_category], + flirted[:weight], + flirted[:non_machinable] + ) + end + else + 0 + end + end + + unless options[:payment_account].check_funds_available(indicia_cost) + raise "...we're out of money (ask Nora to put at least #{ActiveSupport::NumberHelper.number_to_currency(indicia_cost)} in the #{options[:payment_account].display_name} account!)" + end + + purchase_batch_indicia(options[:payment_account]) + end + + # Generate PDF labels with the provided options + generate_labels(options) + + mark_processed! + end + + def regenerate_labels!(options = {}) + labels_pdf.purge + generate_labels(options) + end + + # Purchase indicia for all letters in the batch using a single payment token + def purchase_batch_indicia(payment_account) + # Create a single payment token for the entire batch + payment_token = payment_account.create_payment_token + + # Preload associations to avoid N+1 queries + letters.includes(:address).each do |letter| + next unless letter.postage_type == "indicia" && letter.usps_indicium.nil? + + # Create and purchase indicia for each letter using the same payment token + indicium = USPS::Indicium.new( + letter: letter, + payment_account: payment_account, + mailing_date: letter_mailing_date, + ) + indicium.buy!(payment_token) + end + end + + def total_cost + postage_cost + end + + def postage_cost + # Preload associations to avoid N+1 queries + letters.includes(:address, :usps_indicium).sum do |letter| + if letter.postage_type == "indicia" + if letter.usps_indicium.present? + # Use actual indicia price if indicia are bought + letter.usps_indicium.postage + letter.usps_indicium.fees + elsif letter.address.us? + # For US mail without bought indicia, use metered price + USPS::PricingEngine.metered_price( + letter.processing_category, + letter.weight, + letter.non_machinable + ) + else + # For international mail without bought indicia, use FLIRT-ed price + flirted = letter.flirt + USPS::PricingEngine.metered_price( + flirted[:processing_category], + flirted[:weight], + flirted[:non_machinable] + ) + end + else + # For stamps, use stamp price for US and desired price for international + if letter.address.us? + USPS::PricingEngine.domestic_stamp_price( + letter.processing_category, + letter.weight, + letter.non_machinable + ) + else + USPS::PricingEngine.fcmi_price( + letter.processing_category, + letter.weight, + letter.address.country + ) + end + end + end + end + + def postage_cost_difference(us_postage_type: nil, intl_postage_type: nil) + # Preload associations to avoid N+1 queries + letters.includes(:address, :usps_indicium).each_with_object({ us: 0, intl: 0 }) do |letter, differences| + # Determine what postage type this letter would use + effective_postage_type = if letter.address.us? + us_postage_type || letter.postage_type + else + intl_postage_type || letter.postage_type + end + + # Skip if not switching to indicia + next unless effective_postage_type == "indicia" + + if letter.address.us? + # For US mail: + # Retail price is stamp_price + retail_price = USPS::PricingEngine.domestic_stamp_price( + letter.processing_category, + letter.weight, + letter.non_machinable + ) + + # Indicia price is metered_price + indicia_price = if letter.usps_indicium.present? + letter.usps_indicium.postage + else + USPS::PricingEngine.metered_price( + letter.processing_category, + letter.weight, + letter.non_machinable + ) + end + + # Difference should be negative (savings) + differences[:us] += indicia_price - retail_price + else + # For international mail: + # Retail price is desired_price + retail_price = USPS::PricingEngine.fcmi_price( + letter.processing_category, + letter.weight, + letter.address.country + ) + + # Indicia price is flirted price (higher than retail) + indicia_price = if letter.usps_indicium.present? + letter.usps_indicium.postage + else + # Use flirt to get the closest US price that's higher than the FCMI rate + flirted = letter.flirt + USPS::PricingEngine.metered_price( + flirted[:processing_category], + flirted[:weight], + flirted[:non_machinable] + ) + end + + # Difference should be positive (additional cost) + differences[:intl] += indicia_price - retail_price + end + end + end + + def mailing_date_not_in_past + if letter_mailing_date < Date.current + errors.add(:letter_mailing_date, "cannot be in the past") + end + end + + def default_mailing_date + now = Time.current.in_time_zone("Eastern Time (US & Canada)") + today = now.to_date + + # If it's before 4PM EST on a business day, default to today + if now.hour < 16 && today.on_weekday? + today + else + # Otherwise, default to next business day + next_business_day = today + loop do + next_business_day += 1 + break if next_business_day.on_weekday? + end + next_business_day + end + end + + private + + def update_letter_tags + letters.update_all(tags: tags) + end + + def address_fields + # Only include address fields and rubber_stamps for letter mapping + ["rubber_stamps"] + end + + def build_mapping(row, address) + # Build letter with batch-level specs and extra data + letters.build( + height: letter_height, + width: letter_width, + weight: letter_weight, + processing_category: letter_processing_category, + recipient_email: row&.dig(field_mapping["email"]), + address: address, + usps_mailer_id: mailer_id, + return_address: letter_return_address, + return_address_name: letter_return_address_name, + rubber_stamps: row&.dig(field_mapping["rubber_stamps"]), + tags: tags, + user: user, + ) + end + + def generate_labels(options = {}) + return unless letters.any? + + # Preload associations to avoid N+1 queries + preloaded_letters = letters.includes(:address, :usps_mailer_id) + + # Build options for label generation + label_options = {} + + # Add template information + if template_cycle.present? + label_options[:template_cycle] = template_cycle + elsif template.present? + label_options[:template] = template + end + + # Use the SnailMail service to generate labels + pdf = SnailMail::Service.generate_batch_labels( + preloaded_letters, + label_options.merge(options) + ) + + # Directly attach the PDF to this batch + attach_pdf(pdf.render) + + # Return the PDF + pdf + end +end diff --git a/app/models/letter/instant_queue.rb b/app/models/letter/instant_queue.rb new file mode 100644 index 0000000..94ed6ca --- /dev/null +++ b/app/models/letter/instant_queue.rb @@ -0,0 +1,141 @@ +# == Schema Information +# +# Table name: letter_queues +# +# id :bigint not null, primary key +# include_qr_code :boolean default(TRUE) +# letter_height :decimal(, ) +# letter_mailing_date :date +# letter_processing_category :integer +# letter_return_address_name :string +# letter_weight :decimal(, ) +# letter_width :decimal(, ) +# name :string +# postage_type :string +# slug :string +# tags :citext default([]), is an Array +# template :string +# type :string +# user_facing_title :string +# created_at :datetime not null +# updated_at :datetime not null +# letter_mailer_id_id :bigint +# letter_return_address_id :bigint +# user_id :bigint not null +# usps_payment_account_id :bigint +# +# Indexes +# +# index_letter_queues_on_letter_mailer_id_id (letter_mailer_id_id) +# index_letter_queues_on_letter_return_address_id (letter_return_address_id) +# index_letter_queues_on_type (type) +# index_letter_queues_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (letter_mailer_id_id => usps_mailer_ids.id) +# fk_rails_... (letter_return_address_id => return_addresses.id) +# fk_rails_... (user_id => users.id) +# fk_rails_... (usps_payment_account_id => usps_payment_accounts.id) +# +class Letter::InstantQueue < Letter::Queue + # TODO: drop mailing date, wtf? + + # Validations + validates :template, presence: true + validates :postage_type, presence: true, inclusion: { in: %w[indicia stamps international_origin] } + validates :usps_payment_account_id, presence: true, if: :indicia? + validates :letter_mailing_date, presence: true, if: :indicia? + + # Associations + belongs_to :usps_payment_account, class_name: "USPS::PaymentAccount", optional: true + + # Scopes + default_scope { where(type: "Letter::InstantQueue") } + + # Methods + def indicia? + postage_type == "indicia" + end + + def process_letter_instantly!(address, params = {}) + Rails.logger.info("Starting process_letter_instantly! with postage_type: #{postage_type}") + + letter = ActiveRecord::Base.transaction do + # Create letter directly in pending state + letter = letters.build( + address: address, + height: letter_height, + width: letter_width, + weight: letter_weight, + return_address: letter_return_address, + return_address_name: letter_return_address_name, + usps_mailer_id: letter_mailer_id, + processing_category: letter_processing_category, + tags: tags, + aasm_state: "pending", + postage_type: postage_type, + mailing_date: Date.current + 1.day, + **params, + ) + letter.save! + Rails.logger.info("Created letter #{letter.id} with postage_type: #{letter.postage_type}") + + # Purchase indicia if needed + if indicia? + Rails.logger.info("Creating indicia for letter #{letter.id}") + begin + payment_account = USPS::PaymentAccount.find(usps_payment_account_id) + Rails.logger.info("Found payment account #{payment_account.id}") + + # Create and save the indicium first + indicium = USPS::Indicium.create!( + letter: letter, + payment_account: payment_account, + mailing_date: letter.mailing_date, + ) + Rails.logger.info("Created indicium #{indicium.id} for letter #{letter.id}") + + # Then buy the indicium + indicium.buy! + Rails.logger.info("Successfully bought indicium for letter #{letter.id}") + + # Reload the letter to ensure we have the latest indicium association + letter.reload + if letter.usps_indicium.present? + Rails.logger.info("Verified indicium #{letter.usps_indicium.id} is associated with letter #{letter.id}") + else + Rails.logger.error("Indicium was not properly associated with letter #{letter.id} after creation") + Rails.logger.error("Letter postage_type: #{letter.postage_type}") + Rails.logger.error("Letter mailing_date: #{letter.mailing_date}") + raise "Failed to associate indicium with letter" + end + rescue => e + Rails.logger.error("Failed to create indicium for letter #{letter.id}: #{e.message}") + Rails.logger.error(e.backtrace.join("\n")) + uuid = Honeybadger.notify(e) + raise "Failed to create indicium (please report EID: #{uuid} immediately)" + end + end + letter + end + + # Verify indicium exists before generating label if using indicia + letter.reload + Rails.logger.info("Before generate_label - Letter #{letter.id} postage_type: #{letter.postage_type}") + Rails.logger.info("Before generate_label - Letter #{letter.id} has indicium: #{letter.usps_indicium.present?}") + + if indicia? && !letter.usps_indicium.present? + Rails.logger.error("No indicium found for letter #{letter.id} before generating label") + Rails.logger.error("Letter postage_type: #{letter.postage_type}") + Rails.logger.error("Letter mailing_date: #{letter.mailing_date}") + raise "No indicium found for letter before generating label" + end + + letter.generate_label( + template: template, + include_qr_code: include_qr_code, + ) + letter + end +end diff --git a/app/models/letter/queue.rb b/app/models/letter/queue.rb new file mode 100644 index 0000000..782859b --- /dev/null +++ b/app/models/letter/queue.rb @@ -0,0 +1,117 @@ +# == Schema Information +# +# Table name: letter_queues +# +# id :bigint not null, primary key +# include_qr_code :boolean default(TRUE) +# letter_height :decimal(, ) +# letter_mailing_date :date +# letter_processing_category :integer +# letter_return_address_name :string +# letter_weight :decimal(, ) +# letter_width :decimal(, ) +# name :string +# postage_type :string +# slug :string +# tags :citext default([]), is an Array +# template :string +# type :string +# user_facing_title :string +# created_at :datetime not null +# updated_at :datetime not null +# letter_mailer_id_id :bigint +# letter_return_address_id :bigint +# user_id :bigint not null +# usps_payment_account_id :bigint +# +# Indexes +# +# index_letter_queues_on_letter_mailer_id_id (letter_mailer_id_id) +# index_letter_queues_on_letter_return_address_id (letter_return_address_id) +# index_letter_queues_on_type (type) +# index_letter_queues_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (letter_mailer_id_id => usps_mailer_ids.id) +# fk_rails_... (letter_return_address_id => return_addresses.id) +# fk_rails_... (user_id => users.id) +# fk_rails_... (usps_payment_account_id => usps_payment_accounts.id) +# +class Letter::Queue < ApplicationRecord + belongs_to :user + has_many :letters, foreign_key: :letter_queue_id + has_many :letter_batches, class_name: "Letter::Batch", foreign_key: :letter_queue_id + belongs_to :letter_mailer_id, class_name: "USPS::MailerId", foreign_key: "letter_mailer_id_id", optional: true + belongs_to :letter_return_address, class_name: "ReturnAddress", optional: true + + before_validation :set_slug, on: :create + + validates :slug, uniqueness: true, presence: true + validates :letter_height, :letter_width, :letter_weight, presence: true, numericality: { greater_than: 0 } + validates :letter_mailer_id, presence: true + validates :letter_return_address, presence: true, on: :process + validates :letter_processing_category, presence: true + validates :tags, presence: true, length: { minimum: 1 } + validate :type_cannot_be_changed, on: :update + + def create_letter!(address, params) + letter = letters.build( + address:, + height: letter_height, + width: letter_width, + weight: letter_weight, + return_address: letter_return_address, + return_address_name: letter_return_address_name, + usps_mailer_id: letter_mailer_id, + processing_category: letter_processing_category, + tags: tags, + aasm_state: "queued", + **params, + ) + letter.save! + letter + end + + def make_batch(user:) + ActiveRecord::Base.transaction do + batch = letter_batches.build( + aasm_state: :fields_mapped, + letter_height: letter_height, + letter_width: letter_width, + letter_weight: letter_weight, + letter_processing_category: letter_processing_category, + letter_mailer_id_id: letter_mailer_id_id, + letter_return_address_id: letter_return_address_id, + letter_return_address_name: letter_return_address_name, + user_facing_title: user_facing_title, + tags: tags, + letter_queue_id: id, + user: user, + ) + batch.save! + letters.queued.each do |letter| + letter.batch_id = batch.id + letter.batch_from_queue + letter.save! + end + batch + end + end + + def to_param + slug + end + + private + + def set_slug + self.slug = self.name.parameterize + end + + def type_cannot_be_changed + if type_changed? && persisted? + errors.add(:type, "cannot be changed after creation") + end + end +end diff --git a/app/models/lsv.rb b/app/models/lsv.rb new file mode 100644 index 0000000..598a8cb --- /dev/null +++ b/app/models/lsv.rb @@ -0,0 +1,17 @@ +module LSV + SLUGS = { + msr: MarketingShipmentRequest, + hs: HighSeasShipment, + boba: BobaDropsShipment, + oo: OneOffShipment, + pf: PrintfulShipment + } + + INVERSE = SLUGS.invert.freeze + + def self.slug_for(lsv) + s = INVERSE[lsv.class.responsible_class] + end + + TYPES = SLUGS.values.freeze +end \ No newline at end of file diff --git a/app/models/lsv/base.rb b/app/models/lsv/base.rb new file mode 100644 index 0000000..6bf243c --- /dev/null +++ b/app/models/lsv/base.rb @@ -0,0 +1,104 @@ +module LSV + class Base < Norairrecord::Table + def inspect + "<#{self.class.name} #{id}> #{fields.inspect}" + end + + def to_partial_path + "lsv/type/base" + end + + class << self + def responsible_class + # this is FUCKING DISGUSTING + return self if superclass == Base + if superclass.singleton_class.method_defined?(:responsible_class) + superclass.responsible_class + else + self + end + end + + def records(**args) + return [] unless table_name + raise "don't use Shipment directly!" unless self < Base + super + end + + def email_column + responsible_class.instance_variable_get(:@email_column) + end + + attr_writer :email_column + + def find_by_email(email) + raise ArgumentError, "no email?" if email.nil? || email.empty? + records :filter => "LOWER(TRIM({#{self.email_column}}))='#{email.downcase}'" + end + end + + def tracking_number + nil + end + + def tracking_link + nil + end + + def status_text + "error fetching status! poke nora" + end + + def source_url + fields.dig("source_rec_url", "url") + end + + def source_id + source_url&.split("/").last + end + + def icon + "📦" + end + + def hide_contents? + false + end + + def status_icon + "?" + end + + def shipped? + nil + end + + def description + nil + end + + def email + fields[self.class.email_column] + end + + def to_json(options = {}) + { + id:, + date:, + tracking_link:, + tracking_number:, + type: self.class.name, + type_text:, + title: title_text, + shipped: shipped?, + icon:, + description:, + source_record: source_url, + }.compact.to_json + end + + def to_param + id + end + end +end diff --git a/app/models/lsv/boba_drops_shipment.rb b/app/models/lsv/boba_drops_shipment.rb new file mode 100644 index 0000000..23b6d94 --- /dev/null +++ b/app/models/lsv/boba_drops_shipment.rb @@ -0,0 +1,65 @@ +module LSV + class BobaDropsShipment < Base + self.base_key = Rails.application.credentials.dig(:lsv, :sv_base) + self.table_name = Rails.application.credentials.dig(:lsv, :boba_table) + self.email_column = "Email" + + def title_text + "Boba Drops!" + end + + def type_text + "Boba Drops Shipment" + end + + def date + self["[Shipment Viewer] Approved/pending at"] || "error!" + end + + def status_text + case fields["Physical Status"] + when "Pending" + "pending!" + when "Packed" + "labelled!" + when "Shipped" + "shipped!" + else + "please contact leow@hackclub.com, something went wrong!" + end + end + + def status_icon + case fields["Physical Status"] + when "Pending" + '' + when "Packed" + '' + when "Shipped" + '' + else + '' + end + end + + def tracking_link + fields["[INTL] Tracking Link"] + end + + def tracking_number + fields["[INTL] Tracking ID"] + end + + def icon + "🧋" + end + + def shipped? + fields["Physical Status"] == "Shipped" + end + + def description + "shipment from boba drops <3" + end + end +end diff --git a/app/models/lsv/high_seas_shipment.rb b/app/models/lsv/high_seas_shipment.rb new file mode 100644 index 0000000..acfdd15 --- /dev/null +++ b/app/models/lsv/high_seas_shipment.rb @@ -0,0 +1,76 @@ +module LSV + class HighSeasShipment < Base + self.base_key = Rails.application.credentials.dig(:lsv, :sv_base) + self.table_name = Rails.application.credentials.dig(:lsv, :hso_table) + self.email_column = "recipient:email" + + has_subtypes "shop_item:fulfillment_type", { + ["minuteman"] => "LSV::HsMinutemanShipment", + ["hq_mail"] => "LSV::HsHqMailShipment", + ["third_party_physical"] => "LSV::HsThirdPartyPhysicalShipment", + ["agh"] => "LSV::HsRawPendingAghShipment", + ["agh_random_stickers"] => "LSV::HsRawPendingAghShipment", + } + + def type_text + "High Seas order" + end + + def title_text + "High Seas – #{fields["shop_item:name"] || "unknown?!"}" + end + + def date + self["created_at"] + end + + def status_text + case fields["status"] + when "PENDING_MANUAL_REVIEW", "on_hold" + "awaiting manual review..." + when "AWAITING_YSWS_VERIFICATION" + "waiting for you to get verified..." + when "pending_nightly" + "we'll send it out when we can!" + when "fulfilled" + ["sent!", "mailed!", "on its way!"].sample + else + super + end + end + + def status_icon + case fields["status"] + when "PENDING_MANUAL_REVIEW", "on_hold" + '' + when "AWAITING_YSWS_VERIFICATION" + '' + when "pending_nightly" + '' + when "fulfilled" + '' + end + end + + def tracking_number + fields["tracking_number"] + end + + def tracking_link + tracking_number && "https://parcelsapp.com/en/tracking/#{tracking_number}" + end + + def icon + return "🎁" if fields["shop_item:name"]&.start_with? "Free" + super + end + + def shipped? + fields["status"] == "fulfilled" + end + + def internal_info_partial + :_highseas_internal_info + end + end +end diff --git a/app/models/lsv/hs_hq_mail_shipment.rb b/app/models/lsv/hs_hq_mail_shipment.rb new file mode 100644 index 0000000..7b38dca --- /dev/null +++ b/app/models/lsv/hs_hq_mail_shipment.rb @@ -0,0 +1,25 @@ +module LSV + class HsHqMailShipment < HighSeasShipment + def type_text + "High Seas shipment (from HQ)" + end + + def status_text + case fields["status"] + when "pending_nightly" + ["we'll ship it when we can!", "will be sent when dinobox gets around to it"].sample + else + super + end + end + + def status_icon + case fields["status"] + when "fulfilled" + '' + else + super + end + end + end +end diff --git a/app/models/lsv/hs_minuteman_shipment.rb b/app/models/lsv/hs_minuteman_shipment.rb new file mode 100644 index 0000000..6bb55b8 --- /dev/null +++ b/app/models/lsv/hs_minuteman_shipment.rb @@ -0,0 +1,29 @@ +module LSV + class HsMinutemanShipment < HighSeasShipment + def status_text + case fields["status"] + when "pending_nightly" + "will go out in next week's batch..." + when "fulfilled" + "has gone out/will go out over the next week!" + else + super + end + end + + def status_icon + case fields["status"] + when "pending_nightly" + '' + when "fulfilled" + '' + else + super + end + end + + def icon + "��" + end + end +end diff --git a/app/models/lsv/hs_raw_pending_agh_shipment.rb b/app/models/lsv/hs_raw_pending_agh_shipment.rb new file mode 100644 index 0000000..35ba323 --- /dev/null +++ b/app/models/lsv/hs_raw_pending_agh_shipment.rb @@ -0,0 +1,21 @@ +module LSV + class HsRawPendingAghShipment < HighSeasShipment + def type_text + "Pending warehouse shipment" + end + + def status_text + case fields["status"] + when "pending_nightly" + "will be sent to the warehouse with the next batch!" + else + super + end + end + + def status_icon + return '' if fields["status"] == "pending_nightly" + super + end + end +end diff --git a/app/models/lsv/hs_third_party_physical_shipment.rb b/app/models/lsv/hs_third_party_physical_shipment.rb new file mode 100644 index 0000000..82544c1 --- /dev/null +++ b/app/models/lsv/hs_third_party_physical_shipment.rb @@ -0,0 +1,18 @@ +module LSV + class HsThirdPartyPhysicalShipment < HighSeasShipment + def type_text + "High Seas 3rd-party physical" + end + + def status_text + case fields["status"] + when "pending_nightly" + "will be ordered soon..." + when "fulfilled" + "ordered!" + else + super + end + end + end +end diff --git a/app/models/lsv/marketing_shipment_request.rb b/app/models/lsv/marketing_shipment_request.rb new file mode 100644 index 0000000..1386055 --- /dev/null +++ b/app/models/lsv/marketing_shipment_request.rb @@ -0,0 +1,88 @@ +module LSV + class MarketingShipmentRequest < Base + def to_partial_path + "lsv/type/msr" + end + + self.base_key = Rails.application.credentials.dig(:lsv, :sv_base) + self.table_name = Rails.application.credentials.dig(:lsv, :msr_table) + self.email_column = "Email" + + def type_text + "Warehouse" + end + + def title_text + fields["user_facing_title"] || fields["Request Type"]&.join(", ") || "Who knows?" + end + + def date + self["Date Requested"] + end + + def status_text + case fields["state"] + when "dispatched" + "sent to warehouse..." + when "mailed" + "shipped!" + when "ON_HOLD" + "on hold... contact us for more info!" + when "canceled" + "canceled?" + else + "this shouldn't happen." + end + end + + def status_icon + case fields["state"] + when "dispatched" + '' + when "mailed" + '' + else + '' + end + end + + def tracking_link + fields["Warehouse–Tracking URL"] + end + + def tracking_number + fields["Warehouse–Tracking Number"] unless fields["Warehouse–Tracking Number"] == "Not Provided" + end + + def hide_contents? + fields["surprise"] + end + + def country + FrickinCountryNames.find_country(fields["Country"])&.alpha2 || "US" + end + + def icon + return "🎁" if hide_contents? || title_text.start_with?("High Seas – Free") + return "💵" if fields["Request Type"]&.include?("Boba Drop grant") + return "✉️" if fields["Warehouse–Service"]&.include?("First Class") + "📦" + end + + def shipped? + fields["state"] == "mailed" + end + + def description + return "it's a surprise!" if hide_contents? + begin + fields["user_facing_description"] || + fields["Warehouse–Items Shipped JSON"] && JSON.parse(fields["Warehouse–Items Shipped JSON"]).select { |item| (item["quantity"]&.to_i || 0) > 0 }.map do |item| + "#{item["quantity"]}x #{item["name"]}" + end + rescue JSON::ParserError + "error parsing JSON for #{source_id}!" + end + end + end +end diff --git a/app/models/lsv/one_off_shipment.rb b/app/models/lsv/one_off_shipment.rb new file mode 100644 index 0000000..8085aee --- /dev/null +++ b/app/models/lsv/one_off_shipment.rb @@ -0,0 +1,27 @@ +module LSV + class OneOffShipment < Base + self.base_key = Rails.application.credentials.dig(:lsv, :sv_base) + self.table_name = Rails.application.credentials.dig(:lsv, :oo_table) + self.email_column = "email" + + SUPPORTED_FIELDS = %i[tracking_number status_text icon hide_contents? status_icon shipped? description type_text title_text] + + def tracking_link + fields["tracking_link"] || (tracking_number && "https://parcelsapp.com/en/tracking/#{tracking_number}") || nil + end + + def date + fields["date"] || "2027-01-31" + end + + def icon + fields["icon"] || super + end + + SUPPORTED_FIELDS.each do |field| + define_method field do + fields[field.to_s] + end + end + end +end diff --git a/app/models/lsv/printful_shipment.rb b/app/models/lsv/printful_shipment.rb new file mode 100644 index 0000000..f32e44a --- /dev/null +++ b/app/models/lsv/printful_shipment.rb @@ -0,0 +1,101 @@ +module LSV + class PrintfulShipment < Base + self.base_key = Rails.application.credentials.dig(:lsv, :sv_base) + self.table_name = Rails.application.credentials.dig(:lsv, :pf_table) + self.email_column = "%order:recipient:email" + + def to_partial_path + "lsv/type/printful_shipment" + end + + has_subtypes "subtype", { + "mystic_tavern" => "LSV::MysticTavernShipment", + } + + def date + fields["created"] || Date.parse(fields["%order:created"]).iso8601 + end + + def title_text + "something custom!" + end + + def type_text + "Printful shipment" + end + + def icon + "🧢" + end + + def status_text + case fields["status"] + when "pending" + "pending..." + when "onhold" + "on hold!?" + when "shipped" + "shipped via #{fields["service"]} on #{fields["ship_date"]}!" + when "started" + "in production!" + end + end + + def shipped? + fields["status"] == "shipped" + end + + def status_icon + if shipped? + '' + else + '' + end + end + + def description + order_items = try_to_parse(fields["%order:items"])&.index_by { |item| item["id"] } + shipment_items = try_to_parse(fields["items"]) + + shipment_items.map do |si| + name = order_items&.dig(si["item_id"], "name") || "???" + qty = si["quantity"] + qty != 1 ? "#{qty}x #{name}" : name + end + end + + def tracking_number + fields["tracking_number"] + end + + def tracking_link + fields["tracking_url"] if tracking_number + end + + def internal_info_partial + :_printful_internal_info + end + + private + + def try_to_parse(json) + JSON.parse(json) if json + rescue JSON::ParserError + nil + end + end + + class MysticTavernShipment < PrintfulShipment + def title_text + "Mystic Tavern shirts!" + end + + def type_text + "arrrrrrrrrrrrr" + end + + def icon + "��" + end + end +end diff --git a/app/models/lsv/sprig_shipment.rb b/app/models/lsv/sprig_shipment.rb new file mode 100644 index 0000000..9f1a207 --- /dev/null +++ b/app/models/lsv/sprig_shipment.rb @@ -0,0 +1,55 @@ +module LSV + class SprigShipment < Base + self.base_key = Rails.application.credentials.dig(:lsv, :sv_base) + self.table_name = Rails.application.credentials.dig(:lsv, :sprig_table) + self.email_column = "Email" + + def title_text + "Sprig!" + end + + def type_text + "Sprig shipment" + end + + def date + fields["Created At"] + end + + def status_text + if shipped? + "shipped via #{fields["Carrier"] || "...we don't know"}!" + else + "pending..." + end + end + + def status_icon + if shipped? + '' + else + '' + end + end + + def tracking_link + fields["Tracking"] && "#{(fields["Tracking Base Link"] || "https://parcelsapp.com/en/tracking/")}#{fields["Tracking"]}" + end + + def tracking_number + fields["Tracking"] + end + + def icon + "🌱" + end + + def shipped? + fields["Sprig Status"] == "Shipped" + end + + def description + "a #{fields["Color"]&.downcase.concat " "}Sprig!" + end + end +end diff --git a/app/models/public.rb b/app/models/public.rb new file mode 100644 index 0000000..a0c06df --- /dev/null +++ b/app/models/public.rb @@ -0,0 +1,5 @@ +module Public + def self.table_name_prefix + "public_" + end +end diff --git a/app/models/public/api_key.rb b/app/models/public/api_key.rb new file mode 100644 index 0000000..fa80342 --- /dev/null +++ b/app/models/public/api_key.rb @@ -0,0 +1,60 @@ +# == Schema Information +# +# Table name: public_api_keys +# +# id :bigint not null, primary key +# name :string +# revoked_at :datetime +# token_bidx :string +# token_ciphertext :string +# created_at :datetime not null +# updated_at :datetime not null +# public_user_id :bigint not null +# +# Indexes +# +# index_public_api_keys_on_public_user_id (public_user_id) +# index_public_api_keys_on_token_bidx (token_bidx) UNIQUE +# +# Foreign Keys +# +# fk_rails_... (public_user_id => public_users.id) +# +class Public::APIKey < ApplicationRecord + include Hashid::Rails + belongs_to :public_user, class_name: "Public::User" + + validates :token, presence: true, uniqueness: true + + scope :not_revoked, -> { where(revoked_at: nil).or(where(revoked_at: Time.now..)) } + scope :accessible, -> { not_revoked } + + before_validation :generate_token, on: :create + + TOKEN = ExternalToken.new("apk") + + has_encrypted :token + blind_index :token + + def revoke! + update!(revoked_at: Time.now) + end + + def revoked? + revoked_at.present? + end + + def active? + !revoked? + end + + def abbreviated + "#{token[..15]}.....#{token[-4..]}" + end + + private + + def generate_token + self.token ||= TOKEN.generate + end +end diff --git a/app/models/public/impersonation.rb b/app/models/public/impersonation.rb new file mode 100644 index 0000000..dc3703c --- /dev/null +++ b/app/models/public/impersonation.rb @@ -0,0 +1,22 @@ +# == Schema Information +# +# Table name: public_impersonations +# +# id :bigint not null, primary key +# justification :string +# target_email :string +# created_at :datetime not null +# updated_at :datetime not null +# user_id :bigint not null +# +# Indexes +# +# index_public_impersonations_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (user_id => users.id) +# +class Public::Impersonation < ApplicationRecord + belongs_to :user +end diff --git a/app/models/public/login_code.rb b/app/models/public/login_code.rb new file mode 100644 index 0000000..44696de --- /dev/null +++ b/app/models/public/login_code.rb @@ -0,0 +1,51 @@ +# == Schema Information +# +# Table name: public_login_codes +# +# id :bigint not null, primary key +# expires_at :datetime +# token :string +# used_at :datetime +# created_at :datetime not null +# updated_at :datetime not null +# user_id :bigint not null +# +# Indexes +# +# index_public_login_codes_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (user_id => public_users.id) +# +class Public::LoginCode < ApplicationRecord + belongs_to :user + + validates :token, presence: true, uniqueness: true + validates :expires_at, presence: true + + before_validation :generate_token, on: :create + before_validation :set_expiration, on: :create + + scope :valid, -> { where("expires_at > ? AND used_at IS NULL", Time.current) } + + TOKEN = ExternalToken.new("lc") + + def mark_used! + update!(used_at: Time.current) + end + + def to_param + token + end + + private + + def generate_token + self.token ||= TOKEN.generate + end + + def set_expiration + self.expires_at ||= 30.minutes.from_now + end +end diff --git a/app/models/public/user.rb b/app/models/public/user.rb new file mode 100644 index 0000000..d638c03 --- /dev/null +++ b/app/models/public/user.rb @@ -0,0 +1,19 @@ +# == Schema Information +# +# Table name: public_users +# +# id :bigint not null, primary key +# email :string +# created_at :datetime not null +# updated_at :datetime not null +# +class Public::User < ApplicationRecord + has_many :login_codes + include PublicIdentifiable + + set_public_id_prefix :uzr + + def create_login_code + login_codes.create! + end +end diff --git a/app/models/return_address.rb b/app/models/return_address.rb new file mode 100644 index 0000000..26c1b02 --- /dev/null +++ b/app/models/return_address.rb @@ -0,0 +1,72 @@ +# == Schema Information +# +# Table name: return_addresses +# +# id :bigint not null, primary key +# city :string +# country :integer +# line_1 :string +# line_2 :string +# name :string +# postal_code :string +# shared :boolean +# state :string +# created_at :datetime not null +# updated_at :datetime not null +# user_id :bigint +# +# Indexes +# +# index_return_addresses_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (user_id => users.id) +# +class ReturnAddress < ApplicationRecord + include CountryEnumable + has_country_enum + + belongs_to :user, optional: true + has_many :letters + + # Only validate if the record has at least some data (indicating user attempted to create one) + with_options if: :partially_filled_out? do |address| + address.validates_presence_of :name, :line_1, :city, :state, :postal_code, :country + end + + scope :shared, -> { where(shared: true) } + scope :owned_by, ->(user) { where(user: user) } + + # Add an attribute accessor for the from_letter parameter + attr_accessor :from_letter + + def display_name + "#{name} / #{line_1}" + end + + def format_for_country(other_country) + <<~EOA + #{name} + #{[line_1, line_2].compact_blank.join("\n")} + #{city}, #{state} #{postal_code} + #{country if country != other_country} + EOA + .strip + end + + def location + "#{city}, #{state} #{postal_code} #{country}" + end + + def us? + country == "US" + end + + private + + # Return true if any fields have been filled out, indicating user's intent to create a return address + def partially_filled_out? + [name, line_1, city, state, postal_code].any?(&:present?) + end +end diff --git a/app/models/source_tag.rb b/app/models/source_tag.rb new file mode 100644 index 0000000..13cb69c --- /dev/null +++ b/app/models/source_tag.rb @@ -0,0 +1,17 @@ +# == Schema Information +# +# Table name: source_tags +# +# id :bigint not null, primary key +# name :string +# owner :string +# slug :string +# created_at :datetime not null +# updated_at :datetime not null +# +class SourceTag < ApplicationRecord + def self.web_tag + @web_tag ||= find_by(slug: "theseus_web") + end + has_many :warehouse_orders, class_name: "Warehouse::Order" +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..a051fc3 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,97 @@ +# == Schema Information +# +# Table name: users +# +# id :bigint not null, primary key +# back_office :boolean default(FALSE) +# can_impersonate_public :boolean +# can_warehouse :boolean +# email :string +# icon_url :string +# is_admin :boolean +# username :string +# created_at :datetime not null +# updated_at :datetime not null +# home_mid_id :bigint default(1), not null +# home_return_address_id :bigint default(1), not null +# slack_id :string +# +# Indexes +# +# index_users_on_home_mid_id (home_mid_id) +# index_users_on_home_return_address_id (home_return_address_id) +# +# Foreign Keys +# +# fk_rails_... (home_mid_id => usps_mailer_ids.id) +# fk_rails_... (home_return_address_id => return_addresses.id) +# +class User < ApplicationRecord + has_many :warehouse_templates, class_name: "Warehouse::Template", inverse_of: :user + has_many :return_addresses, dependent: :destroy + has_many :letters + has_many :batches + has_many :letter_queues, dependent: :destroy, class_name: "Letter::Queue" + belongs_to :home_mid, class_name: "USPS::MailerId", optional: true + belongs_to :home_return_address, class_name: "ReturnAddress", optional: true + + include PublicIdentifiable + + set_public_id_prefix "usr" + + def admin? + is_admin + end + + def make_admin! + update!(is_admin: true) + end + + def remove_admin! + update!(is_admin: false) + end + + def self.authorize_url(redirect_uri) + params = { + client_id: ENV["SLACK_CLIENT_ID"], + redirect_uri: redirect_uri, + state: SecureRandom.hex(24), + user_scope: "users.profile:read,users:read,users:read.email", + } + + URI.parse("https://slack.com/oauth/v2/authorize?#{params.to_query}") + end + + def self.from_slack_token(code, redirect_uri) + # Exchange code for token + response = HTTP.post("https://slack.com/api/oauth.v2.access", form: { + client_id: ENV["SLACK_CLIENT_ID"], + client_secret: ENV["SLACK_CLIENT_SECRET"], + code: code, + redirect_uri: redirect_uri, + }) + + data = JSON.parse(response.body.to_s) + + return nil unless data["ok"] + + # Get user info + user_response = HTTP.auth("Bearer #{data["authed_user"]["access_token"]}") + .get("https://slack.com/api/users.info?user=#{data["authed_user"]["id"]}") + + user_data = JSON.parse(user_response.body.to_s) + + return nil unless user_data["ok"] + + user = find_by(slack_id: data.dig("authed_user", "id")) + return nil unless user + + user.email = user_data.dig("user", "profile", "email") + user.username ||= user_data.dig("user", "profile", "username") + user.username ||= user_data.dig("user", "profile", "display_name_normalized") + user.icon_url = user_data.dig("user", "profile", "image_192") || user_data.dig("user", "profile", "image_72") + # Store the OAuth data + user.save! + user + end +end diff --git a/app/models/usps.rb b/app/models/usps.rb new file mode 100644 index 0000000..6224296 --- /dev/null +++ b/app/models/usps.rb @@ -0,0 +1,5 @@ +module USPS + def self.table_name_prefix + "usps_" + end +end diff --git a/app/models/usps/indicium.rb b/app/models/usps/indicium.rb new file mode 100644 index 0000000..258175f --- /dev/null +++ b/app/models/usps/indicium.rb @@ -0,0 +1,99 @@ +# == Schema Information +# +# Table name: usps_indicia +# +# id :bigint not null, primary key +# fees :decimal(, ) +# flirted :boolean +# mailing_date :date +# nonmachinable :boolean +# postage :decimal(, ) +# postage_weight :float +# processing_category :integer +# raw_json_response :jsonb +# usps_sku :string +# created_at :datetime not null +# updated_at :datetime not null +# letter_id :bigint +# usps_payment_account_id :bigint not null +# +# Indexes +# +# index_usps_indicia_on_letter_id (letter_id) +# index_usps_indicia_on_usps_payment_account_id (usps_payment_account_id) +# +# Foreign Keys +# +# fk_rails_... (letter_id => letters.id) +# fk_rails_... (usps_payment_account_id => usps_payment_accounts.id) +# +class USPS::Indicium < ApplicationRecord + include PublicIdentifiable + set_public_id_prefix "ind" + + enum :processing_category, { + letter: 0, + flat: 1, + } + + belongs_to :payment_account, foreign_key: :usps_payment_account_id + belongs_to :letter, optional: true + + def buy!(payment_token = nil) + raise ArgumentError, "for what?" unless letter + payment_token ||= payment_account.create_payment_token + indicium_opts = if letter.address.us? + { + processing_category: usps_proc_cat(letter.processing_category), + weight: letter.weight.to_f, + mailing_date: letter.mailing_date, + length: letter.width.to_f, + height: letter.height.to_f, + thickness: 0.1, + non_machinable_indicators: letter.non_machinable? ? { isRigid: true } : nil, + } + else + flirted = true + attrs = letter.flirt + + { + processing_category: usps_proc_cat(attrs[:processing_category]), + weight: attrs[:weight], + non_machinable_indicators: attrs[:non_machinable] ? { isRigid: true } : nil, + length: 7, + height: 5, + thickness: 0.1, + } + end.compact + + response = USPS::APIService.create_fcm_indicia( + payment_token:, + mailing_date: letter.mailing_date, + image_type: "SVG", + **indicium_opts, + ) + + self.raw_json_response = response + + meta = response[:indiciaMetadata] + self.postage = meta[:postage] + self.fees = meta[:fees]&.sum { |fee| fee[:price] } + self.usps_sku = meta[:SKU] + + save + end + + def cost + (postage || 0) + (fees || 0) + end + + def svg + Base64.decode64(raw_json_response["indiciaImage"]) + end + + private + + def usps_proc_cat(sym) + sym.to_s.pluralize.upcase + end +end diff --git a/app/models/usps/iv_mtr.rb b/app/models/usps/iv_mtr.rb new file mode 100644 index 0000000..84ac210 --- /dev/null +++ b/app/models/usps/iv_mtr.rb @@ -0,0 +1,5 @@ +module USPS::IVMTR + def self.table_name_prefix + "usps_iv_mtr_" + end +end diff --git a/app/models/usps/iv_mtr/event.rb b/app/models/usps/iv_mtr/event.rb new file mode 100644 index 0000000..3fe99a0 --- /dev/null +++ b/app/models/usps/iv_mtr/event.rb @@ -0,0 +1,122 @@ +# == Schema Information +# +# Table name: usps_iv_mtr_events +# +# id :bigint not null, primary key +# happened_at :datetime +# opcode :string +# payload :jsonb +# zip_code :string +# created_at :datetime not null +# updated_at :datetime not null +# batch_id :bigint not null +# letter_id :bigint +# mailer_id_id :bigint not null +# +# Indexes +# +# index_usps_iv_mtr_events_on_batch_id (batch_id) +# index_usps_iv_mtr_events_on_letter_id (letter_id) +# index_usps_iv_mtr_events_on_mailer_id_id (mailer_id_id) +# +# Foreign Keys +# +# fk_rails_... (batch_id => usps_iv_mtr_raw_json_batches.id) +# fk_rails_... (letter_id => letters.id) ON DELETE => nullify +# fk_rails_... (mailer_id_id => usps_mailer_ids.id) +# +class USPS::IVMTR::Event < ApplicationRecord + include PublicIdentifiable + set_public_id_prefix 'mtr' + + + belongs_to :letter, optional: true + belongs_to :batch, class_name: "USPS::IVMTR::RawJSONBatch" + belongs_to :mailer_id, class_name: "USPS::MailerId" + + after_create :notify_slack + + scope :bogons, -> { where(letter_id: nil) } + scope :by_scan_datetime, -> { order(scan_datetime: :desc) } + + def bogon? + letter.nil? + end + + def hydrated + @h ||= IvyMeter::Event::PieceEvent.from_json(payload || {}) + end + + def scan_datetime + hydrated.scan_datetime + end + + def scan_facility + { + name: hydrated.scan_facility_name, + city: hydrated.scan_facility_city, + state: hydrated.scan_facility_state, + zip: hydrated.scan_facility_zip + } + end + + def mail_phase + hydrated.mail_phase + end + + def scan_event_code + hydrated.scan_event_code + end + + def handling_event_type + hydrated.handling_event_type + end + + def handling_event_type_description + hydrated.handling_event_type_description + end + + def imb_serial_number + hydrated.imb_serial_number + end + + def imb_mid + hydrated.imb_mid + end + + def imb_stid + hydrated.imb_stid + end + + def imb + hydrated.imb + end + + def machine_info + { + name: hydrated.machine_name, + id: hydrated.machine_id + } + end + + def self.find_or_create_from_payload(payload, batch_id, mailer_id_id) + event = IvyMeter::Event::PieceEvent.from_json(payload) + letter = Letter.find_by_imb_sn(event.imb_serial_number, USPS::MailerId.find(mailer_id_id)) if event.imb_serial_number + + create!( + payload: payload, + batch_id: batch_id, + mailer_id_id: mailer_id_id, + letter_id: letter&.id, + happened_at: event.scan_datetime, + opcode: event.scan_event_code, + zip_code: event.scan_facility_zip + ) + end + + private + + def notify_slack + USPS::IVMTR::NotifySlackJob.perform_later(self) + end +end diff --git a/app/models/usps/iv_mtr/raw_json_batch.rb b/app/models/usps/iv_mtr/raw_json_batch.rb new file mode 100644 index 0000000..0282543 --- /dev/null +++ b/app/models/usps/iv_mtr/raw_json_batch.rb @@ -0,0 +1,14 @@ +# == Schema Information +# +# Table name: usps_iv_mtr_raw_json_batches +# +# id :bigint not null, primary key +# payload :jsonb +# processed :boolean +# created_at :datetime not null +# updated_at :datetime not null +# message_group_id :string +# +class USPS::IVMTR::RawJSONBatch < ApplicationRecord + has_many :events, class_name: "USPS::IVMTR::Event" +end diff --git a/app/models/usps/mailer_id.rb b/app/models/usps/mailer_id.rb new file mode 100644 index 0000000..b970889 --- /dev/null +++ b/app/models/usps/mailer_id.rb @@ -0,0 +1,41 @@ +# == Schema Information +# +# Table name: usps_mailer_ids +# +# id :bigint not null, primary key +# crid :string +# mid :string +# name :string +# rollover_count :integer +# sequence_number :bigint +# created_at :datetime not null +# updated_at :datetime not null +# +class USPS::MailerId < ApplicationRecord + def display_name + "#{name} (#{crid}/#{mid})" + end + + def sn_length + 15 - mid.length + end + + def max_sn + (10**sn_length) - 1 + end + + def next_sn_and_rollover + transaction do + lock! + self.sequence_number ||= 0 + self.rollover_count ||= 0 + self.sequence_number += 1 + if self.sequence_number > max_sn + self.sequence_number = 1 + self.rollover_count += 1 + end + save! + end + [ sequence_number, rollover_count ] + end +end diff --git a/app/models/usps/payment_account.rb b/app/models/usps/payment_account.rb new file mode 100644 index 0000000..ee0707a --- /dev/null +++ b/app/models/usps/payment_account.rb @@ -0,0 +1,94 @@ +# == Schema Information +# +# Table name: usps_payment_accounts +# +# id :bigint not null, primary key +# account_number :string +# account_type :integer +# ach :boolean +# manifest_mid :string +# name :string +# permit_number :string +# permit_zip :string +# created_at :datetime not null +# updated_at :datetime not null +# usps_mailer_id_id :bigint not null +# +# Indexes +# +# index_usps_payment_accounts_on_usps_mailer_id_id (usps_mailer_id_id) +# +# Foreign Keys +# +# fk_rails_... (usps_mailer_id_id => usps_mailer_ids.id) +# +class USPS::PaymentAccount < ApplicationRecord + belongs_to :usps_mailer_id, class_name: "USPS::MailerId" + + enum :account_type, { + EPS: 0, + PERMIT: 1, + # METER: 2 # someday.... someday i will be a PC Postage vendor..,,,.., + } + + def display_name + case account_type + when "EPS" + "#{name} (#{obscured_last_4(account_number)})" + when "PERMIT" + "#{name} (#{permit_number} @ #{permit_zip})" + else + name + end + end + + alias_method :to_s, :display_name + + def obscured_last_4(text) + last_4 = text.to_s[-4..] + "••••#{last_4}" + end + + validates :account_type, presence: true + validates :account_number, presence: true, if: :EPS? + validates :permit_number, presence: true, if: :PERMIT? + validates :permit_zip, presence: true, if: :PERMIT? + + def create_payment_token + roles = [ + { + roleName: "PAYER", + CRID: usps_mailer_id.crid, + MID: usps_mailer_id.mid, + accountType: account_type.to_s, + accountNumber: account_number, + permitNumber: permit_number, + permitZIP: permit_zip, + manifestMID: manifest_mid || usps_mailer_id.mid, + }.compact_blank, + { + roleName: "LABEL_OWNER", + CRID: usps_mailer_id.crid, + MID: usps_mailer_id.mid, + manifestMID: manifest_mid || usps_mailer_id.mid, + }, + { + roleName: "MAIL_OWNER", + CRID: usps_mailer_id.crid, + MID: usps_mailer_id.mid, + manifestMID: manifest_mid || usps_mailer_id.mid, + }, + ] + USPS::APIService.create_payment_token(roles:) + end + + def check_funds_available(amount) + return true if ach? + USPS::APIService.payment_account_inquiry( + account_number:, + account_type: account_type.to_s, + permit_zip:, + amount:, + )[:sufficientFunds] + end +end diff --git a/app/models/warehouse.rb b/app/models/warehouse.rb new file mode 100644 index 0000000..153ae03 --- /dev/null +++ b/app/models/warehouse.rb @@ -0,0 +1,5 @@ +module Warehouse + def self.table_name_prefix + "warehouse_" + end +end diff --git a/app/models/warehouse/batch.rb b/app/models/warehouse/batch.rb new file mode 100644 index 0000000..c7e3501 --- /dev/null +++ b/app/models/warehouse/batch.rb @@ -0,0 +1,126 @@ +# == Schema Information +# +# Table name: batches +# +# id :bigint not null, primary key +# aasm_state :string +# address_count :integer +# field_mapping :jsonb +# letter_height :decimal(, ) +# letter_mailing_date :date +# letter_processing_category :integer +# letter_return_address_name :string +# letter_weight :decimal(, ) +# letter_width :decimal(, ) +# tags :citext default([]), is an Array +# template_cycle :string default([]), is an Array +# type :string not null +# warehouse_user_facing_title :string +# created_at :datetime not null +# updated_at :datetime not null +# letter_mailer_id_id :bigint +# letter_queue_id :bigint +# letter_return_address_id :bigint +# user_id :bigint not null +# warehouse_template_id :bigint +# +# Indexes +# +# index_batches_on_letter_mailer_id_id (letter_mailer_id_id) +# index_batches_on_letter_queue_id (letter_queue_id) +# index_batches_on_letter_return_address_id (letter_return_address_id) +# index_batches_on_tags (tags) USING gin +# index_batches_on_type (type) +# index_batches_on_user_id (user_id) +# index_batches_on_warehouse_template_id (warehouse_template_id) +# +# Foreign Keys +# +# fk_rails_... (letter_mailer_id_id => usps_mailer_ids.id) +# fk_rails_... (letter_queue_id => letter_queues.id) +# fk_rails_... (letter_return_address_id => return_addresses.id) +# fk_rails_... (user_id => users.id) +# fk_rails_... (warehouse_template_id => warehouse_templates.id) +# +class Warehouse::Batch < Batch + belongs_to :warehouse_template, class_name: "Warehouse::Template" + + has_many :orders, class_name: "Warehouse::Order" + + def self.model_name + Batch.model_name + end + + def process!(options = {}) + return false unless fields_mapped? + + # Create orders for each address + addresses.each do |address| + Warehouse::Order.from_template( + warehouse_template, + batch: self, + recipient_email: address.email, + address: address, + user: user, + idempotency_key: "batch_#{id}_address_#{address.id}", + user_facing_title: warehouse_user_facing_title, + tags: tags, + ).save! + end + + # Dispatch all orders + orders.each do |order| + order.dispatch! + end + + mark_processed! + end + + def build_mapping(row, address) + # For warehouse batches, we just return the address + # Orders will be created during processing + address + end + + def contents_cost + warehouse_template.contents_actual_cost_to_hc * addresses.count + end + + def labor_cost + warehouse_template.labor_cost * addresses.count + end + + def postage_cost + orders.sum(:postage_cost) + end + + def total_cost + contents_cost + labor_cost + postage_cost + end + + def update_associated_tags + orders.update_all(tags: tags) + end + + def process_with_zenventory!(options = {}) + return false unless fields_mapped? + + # Create orders for each address + addresses.each do |address| + begin + Zenventory.create_customer_order(update_hash) + rescue Zenventory::ZenventoryError => e + uuid = Honeybadger.notify(e) + errors.add(:base, "couldn't create order, Zenventory said: #{e.message} (error: #{uuid})") + throw(:abort) + end + end + + # Dispatch all orders + orders.each do |order| + order.dispatch! + end + + mark_processed! + end +end diff --git a/app/models/warehouse/line_item.rb b/app/models/warehouse/line_item.rb new file mode 100644 index 0000000..7bab80c --- /dev/null +++ b/app/models/warehouse/line_item.rb @@ -0,0 +1,32 @@ +# == Schema Information +# +# Table name: warehouse_line_items +# +# id :bigint not null, primary key +# quantity :integer +# created_at :datetime not null +# updated_at :datetime not null +# order_id :bigint +# sku_id :bigint not null +# template_id :bigint +# +# Indexes +# +# index_warehouse_line_items_on_order_id (order_id) +# index_warehouse_line_items_on_sku_id (sku_id) +# index_warehouse_line_items_on_template_id (template_id) +# +# Foreign Keys +# +# fk_rails_... (order_id => warehouse_orders.id) +# fk_rails_... (sku_id => warehouse_skus.id) +# fk_rails_... (template_id => warehouse_templates.id) +# +class Warehouse::LineItem < ApplicationRecord + belongs_to :order, class_name: "Warehouse::Order", optional: true + belongs_to :sku, class_name: "Warehouse::SKU", optional: true + belongs_to :template, optional: true + + validates :quantity, presence: true, numericality: { greater_than: 0 } + validates :sku, presence: true +end diff --git a/app/models/warehouse/order.rb b/app/models/warehouse/order.rb new file mode 100644 index 0000000..a9ceaf3 --- /dev/null +++ b/app/models/warehouse/order.rb @@ -0,0 +1,360 @@ +# == Schema Information +# +# Table name: warehouse_orders +# +# id :bigint not null, primary key +# aasm_state :string +# canceled_at :datetime +# carrier :string +# contents_cost :decimal(10, 2) +# dispatched_at :datetime +# idempotency_key :string +# internal_notes :text +# labor_cost :decimal(10, 2) +# mailed_at :datetime +# metadata :jsonb +# notify_on_dispatch :boolean +# postage_cost :decimal(, ) +# recipient_email :string +# service :string +# surprise :boolean +# tags :citext default([]), is an Array +# tracking_number :string +# user_facing_description :string +# user_facing_title :string +# weight :decimal(, ) +# created_at :datetime not null +# updated_at :datetime not null +# address_id :bigint not null +# batch_id :bigint +# hc_id :string +# source_tag_id :bigint not null +# template_id :bigint +# user_id :bigint not null +# zenventory_id :integer +# +# Indexes +# +# index_warehouse_orders_on_address_id (address_id) +# index_warehouse_orders_on_batch_id (batch_id) +# index_warehouse_orders_on_hc_id (hc_id) +# index_warehouse_orders_on_idempotency_key (idempotency_key) UNIQUE +# index_warehouse_orders_on_source_tag_id (source_tag_id) +# index_warehouse_orders_on_tags (tags) USING gin +# index_warehouse_orders_on_template_id (template_id) +# index_warehouse_orders_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (address_id => addresses.id) +# fk_rails_... (batch_id => batches.id) +# fk_rails_... (source_tag_id => source_tags.id) +# fk_rails_... (template_id => warehouse_templates.id) +# fk_rails_... (user_id => users.id) +# +class Warehouse::Order < ApplicationRecord + include AASM + include HasAddress + include CanBeBatched + include PublicIdentifiable + set_public_id_prefix "pkg" + + belongs_to :template, class_name: "Warehouse::Template", optional: true + belongs_to :user + belongs_to :source_tag + + validates :line_items, presence: true + validates :recipient_email, presence: true + validate :can_mail_parcels_to_country + + after_create :set_hc_id + before_save :update_costs + + include HasWarehouseLineItems + include HasTableSync + include HasZenventoryUrl + include Taggable + + has_table_sync ENV["AIRTABLE_THESEUS_BASE"], + ENV["AIRTABLE_WAREHOUSE_REQUESTS_TABLE"], + { + id: :hc_id, + state: :aasm_state, + recipient: :recipient_email, + contents: :generate_order_items, + created_at: :created_at, + updated_at: :updated_at, + zenventory_id: :zenventory_id, + user_facing_title: :user_facing_title, + tracking_number: :tracking_number, + carrier: :carrier, + service: :service, + mailed_at: :mailed_at, + labor_cost: :labor_cost, + postage_cost: :postage_cost, + } + + has_zenventory_url "https://app.zenventory.com/orders/edit-order/%s", :zenventory_id + + def shipping_address_attributes + { + name: address.name_line, + line1: address.line_1, + line2: address.line_2, + city: address.city, + state: address.state, + zip: address.postal_code, + countryCode: address.country, + phone: address.phone_number, + }.compact_blank + end + + def customer_attributes + { + name: address.first_name, + surname: address.last_name || "​", + email: recipient_email, + }.compact_blank + end + + def cancel!(reason) + transaction do + mark_canceled! + Zenventory.cancel_customer_order(zenventory_id, reason) + end + end + + def dispatch! + ActiveRecord::Base.transaction do + raise AASM::InvalidTransition, "wrong state" unless may_mark_dispatched? + order = Zenventory.create_customer_order( + { + orderNumber: "hack.club/#{hc_id}", + customer: customer_attributes, + shippingAddress: shipping_address_attributes, + billingAddress: { sameAsShipping: true }, + items: generate_order_items, + } + ) + mark_dispatched!(order[:id]) + end + + if notify_on_dispatch? + Warehouse::OrderMailer.with(order: self).order_created.deliver_later + end + end + + def zenv_attributes_changed? + return true if recipient_email_changed? + return true if address&.changed? + return true if line_items.any?(&:marked_for_destruction?) || + line_items.any?(&:new_record?) || + line_items.any?(&:changed?) + false + end + + before_update { |rec| try_zenventory_update! if rec.zenv_attributes_changed? && !rec.draft? } + + def try_zenventory_update! + if mailed? + errors.add(:base, "can't edit an order that's already been shipped!") + throw(:abort) + end + if canceled? + errors.add(:base, "can't edit an order that's canceled!") + throw(:abort) + end + begin + update_hash = { + customer: customer_attributes, + shippingAddress: shipping_address_attributes, + billingAddress: { sameAsShipping: true }, + items: generate_order_items_for_update, + }.compact_blank + Zenventory.update_customer_order(zenventory_id, update_hash) unless update_hash.empty? + rescue Zenventory::ZenventoryError => e + uuid = Honeybadger.notify(e) + errors.add(:base, "couldn't edit order, Zenventory said: #{e.message} (error: #{uuid})") + throw(:abort) + end + end + + def self.from_template(template, attributes) + new( + attributes.merge( + template: template, + source_tag: template.source_tag, + ) + ) + end + + def initialize(attributes = {}) + super + if attributes&.[](:template) + template.line_items.each do |template_line_item| + line_items.build( + sku: template_line_item.sku, + quantity: template_line_item.quantity, + ) + end + end + end + + HUMANIZED_STATES = { + draft: "Draft", + dispatched: "Sent to warehouse", + mailed: "Shipped!", + errored: "Errored?", + canceled: "Canceled", + } + + def humanized_state + HUMANIZED_STATES[aasm_state.to_sym] + end + + aasm timestamps: true do + state :draft, initial: true + state :dispatched + state :mailed + state :errored + state :canceled + + event :mark_dispatched do + transitions from: :draft, to: :dispatched + after do |zenventory_id| + update!(zenventory_id:) + end + end + + event :mark_mailed do + transitions from: :dispatched, to: :mailed + end + + event :mark_canceled do + transitions from: :dispatched, to: :canceled + end + end + + def tracking_format + @tracking_format ||= Tracking.get_format_by_zenv_info(carrier:, service:) + end + + def tracking_url + Tracking.tracking_url_for(tracking_format, tracking_number) + end + + def might_be_slow? + %i[asendia usps].include?(tracking_format) + end + + def pretty_via + case tracking_format + when :usps + "USPS" + when :asendia + "Asendia" + when :ups + "UPS #{service}" + else + "#{carrier} #{service}" + end + end + + def generate_order_items + line_items.map do |line_item| + { + sku: line_item.sku.sku, + price: line_item.sku.declared_unit_cost, + quantity: line_item.quantity, + } + end + end + + # nora: entirely v*becoded because i can't be fucked + def generate_order_items_for_update + # Check if we need to fetch existing order details from Zenventory + has_deletions = line_items.any?(&:marked_for_destruction?) + has_modifications = line_items.reject(&:marked_for_destruction?).any? { |li| li.changed? } + + # Only fetch from Zenventory if we need IDs for updates or deletions + zenventory_item_map = {} + if has_deletions || has_modifications + # Fetch current order from Zenventory to get line item IDs + zenventory_order = Zenventory.get_customer_order(zenventory_id) + zenventory_items = zenventory_order[:items] || [] + + # Use index_by to create a lookup hash of existing Zenventory line items by SKU + zenventory_item_map = zenventory_items.index_by { |item| item[:sku] } + end + + items_to_update = [] + + # Process items marked for deletion + if has_deletions + line_items.select(&:marked_for_destruction?).each do |line_item| + zenv_item = zenventory_item_map[line_item.sku.sku] + next unless zenv_item # Skip if we can't find the item in Zenventory + + items_to_update << create_item_hash(line_item, zenv_item[:id], 0) # Set quantity to 0 for deletion + end + end + + # Process new and modified items + line_items.reject(&:marked_for_destruction?).each do |line_item| + if line_item.new_record? + # New line item - don't include ID + items_to_update << create_item_hash(line_item) + elsif line_item.changed? + # Modified line item - include ID if available + zenv_item = zenventory_item_map[line_item.sku.sku] + items_to_update << create_item_hash(line_item, zenv_item&.dig(:id)) + end + end + + items_to_update + end + + # Helper method just for updates + private def create_item_hash(line_item, item_id = nil, quantity = nil) + item_hash = { + sku: line_item.sku.sku, + price: line_item.sku.declared_unit_cost, + quantity: quantity || line_item.quantity, + } + + # Only include ID if one was provided + item_hash[:id] = item_id if item_id + + item_hash + end + + def total_cost + [contents_cost, labor_cost, postage_cost].compact_blank.sum + end + + def to_param + hc_id + end + + private + + def set_hc_id + update_column(:hc_id, public_id) + end + + def update_costs + # Ensure line items are loaded and include their SKUs + line_items.includes(:sku).load + self.labor_cost = labor_cost + self.contents_cost = contents_actual_cost_to_hc + end + + def can_mail_parcels_to_country + errors.add(:base, :cant_mail, message: "We can't currently ship to #{ISO3166::Country[address.country]&.common_name || address.country} from the warehouse.") if %i[IR PS CU KP RU].include? address.country&.to_sym + end + + def inherit_batch_tags + return unless batch.present? + self.tags = (tags + batch.tags).uniq + end +end diff --git a/app/models/warehouse/sku.rb b/app/models/warehouse/sku.rb new file mode 100644 index 0000000..110051a --- /dev/null +++ b/app/models/warehouse/sku.rb @@ -0,0 +1,66 @@ +# == Schema Information +# +# Table name: warehouse_skus +# +# id :bigint not null, primary key +# actual_cost_to_hc :decimal(, ) +# ai_enabled :boolean +# average_po_cost :decimal(, ) +# category :integer +# country_of_origin :string +# customs_description :text +# declared_unit_cost_override :decimal(, ) +# description :text +# enabled :boolean +# hs_code :string +# in_stock :integer +# inbound :integer +# name :string +# sku :string +# created_at :datetime not null +# updated_at :datetime not null +# zenventory_id :string +# +# Indexes +# +# index_warehouse_skus_on_sku (sku) UNIQUE +# +class Warehouse::SKU < ApplicationRecord + scope :in_inventory, -> { where.not(in_stock: nil, inbound: nil) } + scope :backordered, -> { where("in_stock < 0") } + + def declared_unit_cost + declared_unit_cost_override || average_po_cost || 0.0 + end + + enum :category, { + sticker: 0, + poster: 1, + card: 2, + flyer: 3, + other_printed_material: 4, + hardware: 5, + book: 6, + swag: 7, + grant: 8, + prize: 9, + } + + include HasTableSync + include HasZenventoryUrl + + has_table_sync ENV["AIRTABLE_THESEUS_BASE"], + ENV["AIRTABLE_SKUS_TABLE"], + { + sku: :sku, + name: :name, + category: ->(_) { category.to_s.humanize }, + enabled: :enabled, + declared_unit_cost: :declared_unit_cost, + actual_cost_to_hc: :actual_cost_to_hc, + in_stock: :in_stock, + inbound: :inbound, + } + + has_zenventory_url "https://app.zenventory.com/admin/item-details/%s/basic", :zenventory_id +end diff --git a/app/models/warehouse/template.rb b/app/models/warehouse/template.rb new file mode 100644 index 0000000..fe4c58f --- /dev/null +++ b/app/models/warehouse/template.rb @@ -0,0 +1,29 @@ +# == Schema Information +# +# Table name: warehouse_templates +# +# id :bigint not null, primary key +# name :string +# public :boolean +# created_at :datetime not null +# updated_at :datetime not null +# source_tag_id :bigint not null +# user_id :bigint not null +# +# Indexes +# +# index_warehouse_templates_on_source_tag_id (source_tag_id) +# index_warehouse_templates_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (source_tag_id => source_tags.id) +# fk_rails_... (user_id => users.id) +# +class Warehouse::Template < ApplicationRecord + include HasWarehouseLineItems + scope :shared, -> { where(public: true) } + + belongs_to :user + belongs_to :source_tag +end diff --git a/app/policies/api_key_policy.rb b/app/policies/api_key_policy.rb new file mode 100644 index 0000000..88b54d8 --- /dev/null +++ b/app/policies/api_key_policy.rb @@ -0,0 +1,36 @@ +class APIKeyPolicy < ApplicationPolicy + # NOTE: Up to Pundit v2.3.1, the inheritance was declared as + # `Scope < Scope` rather than `Scope < ApplicationPolicy::Scope`. + # In most cases the behavior will be identical, but if updating existing + # code, beware of possible changes to the ancestors: + # https://gist.github.com/Burgestrand/4b4bc22f31c8a95c425fc0e30d7ef1f5 + + def index? + true + end + + def new? + true + end + + def show? + record_belongs_to_user || user_is_admin + end + + def revoke? + record_belongs_to_user || user_is_admin + end + + alias_method :create?, :new? + alias_method :revoke_confirm?, :revoke? + + class Scope < ApplicationPolicy::Scope + def resolve + if user.is_admin? + scope.all + else + scope.where(user_id: user.id) + end + end + end +end diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb new file mode 100644 index 0000000..335f2c4 --- /dev/null +++ b/app/policies/application_policy.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +class ApplicationPolicy + attr_reader :user, :record + + def initialize(user, record) + @user = user + @record = record + end + + def index? + user_is_admin + end + + def show? + user_is_admin + end + + def create? + false + end + + def new? + create? + end + + def update? + false + end + + def edit? + update? + end + + def destroy? + false + end + + class Scope + def initialize(user, scope) + @user = user + @scope = scope + end + + def resolve + raise NoMethodError, "You must define #resolve in #{self.class}" + end + + private + + attr_reader :user, :scope + end + + def user_is_admin + user&.admin? + end + + def user_can_warehouse + user&.can_warehouse? || user_is_admin + end + + def record_belongs_to_user + user && record&.user == user + end +end diff --git a/app/policies/batch_policy.rb b/app/policies/batch_policy.rb new file mode 100644 index 0000000..b8a4560 --- /dev/null +++ b/app/policies/batch_policy.rb @@ -0,0 +1,92 @@ +class BatchPolicy < ApplicationPolicy + def index? + user_can_warehouse + end + + def show? + return true if user_is_admin + user_can_warehouse && record_belongs_to_user + end + + def new? + user_can_warehouse + end + + def create? + user_can_warehouse + end + + def edit? + return true if user_is_admin + user_can_warehouse && record_belongs_to_user + end + + def update? + return true if user_is_admin + user_can_warehouse && record_belongs_to_user + end + + def destroy? + return true if user_is_admin + user_can_warehouse && record_belongs_to_user + end + + def map_fields? + return true if user_is_admin + user_can_warehouse && record_belongs_to_user + end + + def set_mapping? + return true if user_is_admin + user_can_warehouse && record_belongs_to_user + end + + def process_batch? + return true if user_is_admin + user_can_warehouse && record_belongs_to_user + end + + def process_form? + return true if user_is_admin + user_can_warehouse && record_belongs_to_user + end + + def mark_printed? + return true if user_is_admin + user_can_warehouse && record_belongs_to_user + end + + def mark_mailed? + return true if user_is_admin + user_can_warehouse && record_belongs_to_user + end + + def update_costs? + return true if user_is_admin + user_can_warehouse && record_belongs_to_user + end + + class Scope < ApplicationPolicy::Scope + def resolve + if user&.admin? + scope.all + elsif user_can_warehouse + scope.where(user: user) + else + scope.none + end + end + + private + + def user_can_warehouse + user&.can_warehouse? || user&.admin? + end + end + + private + + def record_belongs_to_user + user && record.user == user + end +end diff --git a/app/policies/customs_receipt_policy.rb b/app/policies/customs_receipt_policy.rb new file mode 100644 index 0000000..1ce538e --- /dev/null +++ b/app/policies/customs_receipt_policy.rb @@ -0,0 +1,20 @@ +class CustomsReceiptPolicy < ApplicationPolicy + # NOTE: Up to Pundit v2.3.1, the inheritance was declared as + # `Scope < Scope` rather than `Scope < ApplicationPolicy::Scope`. + # In most cases the behavior will be identical, but if updating existing + # code, beware of possible changes to the ancestors: + # https://gist.github.com/Burgestrand/4b4bc22f31c8a95c425fc0e30d7ef1f5 + + def index? + user&.can_warehouse? + end + + alias_method :generate?, :index? + + class Scope < ApplicationPolicy::Scope + # NOTE: Be explicit about which records you allow access to! + # def resolve + # scope.all + # end + end +end diff --git a/app/policies/letter/batch_policy.rb b/app/policies/letter/batch_policy.rb new file mode 100644 index 0000000..dba774b --- /dev/null +++ b/app/policies/letter/batch_policy.rb @@ -0,0 +1,69 @@ +class Letter::BatchPolicy < ApplicationPolicy + def index? + user.present? + end + + def show? + user.present? + end + + def new? + user.present? + end + + def create? + user.present? + end + + def edit? + user.present? + end + + def update? + user.present? + end + + def destroy? + user.admin? + end + + def map_fields? + user.present? + end + + def set_mapping? + user.present? + end + + def process_form? + user.present? + end + + def process_batch? + user.present? + end + + def mark_printed? + user.present? + end + + def mark_mailed? + user.present? + end + + def update_costs? + user.present? + end + + class Scope < ApplicationPolicy::Scope + def resolve + if user&.admin? + scope.all + elsif user.present? + scope.where(user: user) + else + scope.none + end + end + end +end diff --git a/app/policies/letter/queue_policy.rb b/app/policies/letter/queue_policy.rb new file mode 100644 index 0000000..92eaeac --- /dev/null +++ b/app/policies/letter/queue_policy.rb @@ -0,0 +1,33 @@ +class Letter::QueuePolicy < ApplicationPolicy + # NOTE: Up to Pundit v2.3.1, the inheritance was declared as + # `Scope < Scope` rather than `Scope < ApplicationPolicy::Scope`. + # In most cases the behavior will be identical, but if updating existing + # code, beware of possible changes to the ancestors: + # https://gist.github.com/Burgestrand/4b4bc22f31c8a95c425fc0e30d7ef1f5 + + def create_letter? + user.present? + end + + alias_method :create_instant_letter?, :create_letter? + + def batch? + record_belongs_to_user || user_is_admin + end + + def mark_printed_instants_mailed? + user_is_admin + end + + class Scope < ApplicationPolicy::Scope + def resolve + if user&.admin? + scope.all + elsif user.present? + scope.where(user: user) + else + scope.none + end + end + end +end diff --git a/app/policies/letter_policy.rb b/app/policies/letter_policy.rb new file mode 100644 index 0000000..b41daa3 --- /dev/null +++ b/app/policies/letter_policy.rb @@ -0,0 +1,79 @@ +class LetterPolicy < ApplicationPolicy + def index? + true + end + + def show? + true + end + + def new? + true + end + + def create? + true + end + + def by_tag? + true + end + + def edit? + record_belongs_to_user || user_is_admin + end + + def update? + record_belongs_to_user || user_is_admin + end + + def destroy? + user_is_admin + end + + def generate_label? + record_belongs_to_user || user_is_admin + end + + def buy_indicia? + record_belongs_to_user || user_is_admin + end + + def mark_printed? + record_belongs_to_user || user_is_admin + end + + def mark_mailed? + record_belongs_to_user || user_is_admin + end + + def mark_received? + record_belongs_to_user || user_is_admin + end + + def clear_label? + record_belongs_to_user || user_is_admin + end + + def preview_template? + Rails.env.development? && (record_belongs_to_user || user_is_admin) + end + + class Scope < ApplicationPolicy::Scope + def resolve + if user&.admin? + scope.all + elsif user.present? + scope.where(user: user) + else + scope.none + end + end + end + + private + + def record_belongs_to_user + user && (record.user == user || record.batch&.user == user) + end +end diff --git a/app/policies/public/impersonation_policy.rb b/app/policies/public/impersonation_policy.rb new file mode 100644 index 0000000..1a96436 --- /dev/null +++ b/app/policies/public/impersonation_policy.rb @@ -0,0 +1,8 @@ +module Public + class ImpersonationPolicy < ApplicationPolicy + def new? + user&.admin? || user&.can_impersonate_public? + end + alias_method :create?, :new? + end +end \ No newline at end of file diff --git a/app/policies/public/user_policy.rb b/app/policies/public/user_policy.rb new file mode 100644 index 0000000..facc943 --- /dev/null +++ b/app/policies/public/user_policy.rb @@ -0,0 +1,3 @@ +class Public::UserPolicy < ApplicationPolicy + +end diff --git a/app/policies/return_address_policy.rb b/app/policies/return_address_policy.rb new file mode 100644 index 0000000..0868d8a --- /dev/null +++ b/app/policies/return_address_policy.rb @@ -0,0 +1,39 @@ +class ReturnAddressPolicy < ApplicationPolicy + def index? + true + end + + def show? + true + end + + def create? + true + end + + def new? + create? + end + + def update? + user_is_admin || record_belongs_to_user + end + + def edit? + update? + end + + def destroy? + user_is_admin || record_belongs_to_user + end + + def set_as_home? + record_belongs_to_user || record.shared? + end + + private + + def record_belongs_to_user + user && record.user == user + end +end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb new file mode 100644 index 0000000..99a89ba --- /dev/null +++ b/app/policies/user_policy.rb @@ -0,0 +1,5 @@ +class UserPolicy < ApplicationPolicy + def show? + user&.admin? || record == user + end +end diff --git a/app/policies/warehouse/batch_policy.rb b/app/policies/warehouse/batch_policy.rb new file mode 100644 index 0000000..6993499 --- /dev/null +++ b/app/policies/warehouse/batch_policy.rb @@ -0,0 +1,75 @@ +class Warehouse::BatchPolicy < ApplicationPolicy + def index? + user_can_warehouse + end + + def show? + user_can_warehouse + end + + def new? + user_can_warehouse + end + + def create? + user_can_warehouse + end + + def edit? + user_can_warehouse + end + + def update? + user_can_warehouse + end + + def destroy? + user_is_admin + end + + def map_fields? + user_can_warehouse + end + + def set_mapping? + user_can_warehouse + end + + def process_form? + user_can_warehouse + end + + def process_batch? + user_can_warehouse + end + + def mark_printed? + user_can_warehouse + end + + def mark_mailed? + user_can_warehouse + end + + def update_costs? + user_can_warehouse + end + + class Scope < ApplicationPolicy::Scope + def resolve + if user&.admin? + scope.all + elsif user_can_warehouse + scope.where(user: user) + else + scope.none + end + end + + private + + def user_can_warehouse + user&.can_warehouse? || user&.admin? + end + end +end diff --git a/app/policies/warehouse/order_policy.rb b/app/policies/warehouse/order_policy.rb new file mode 100644 index 0000000..6537e65 --- /dev/null +++ b/app/policies/warehouse/order_policy.rb @@ -0,0 +1,61 @@ +class Warehouse::OrderPolicy < ApplicationPolicy + def new? + user_can_warehouse + end + + def create? + user_can_warehouse + end + + def edit? + return false unless user_can_warehouse + record_belongs_to_user || user_is_admin + end + + def update? + return false unless user_can_warehouse + record_belongs_to_user || user_is_admin + end + + def show? + user_can_warehouse + end + + def index? + user_can_warehouse + end + + def cancel? + return false unless user_can_warehouse + record_belongs_to_user || user_is_admin + end + + def send_to_warehouse? + return false unless user_can_warehouse + record_belongs_to_user || user_is_admin + end + + def destroy? + return false unless record.draft? + return false unless user_can_warehouse + record_belongs_to_user || user_is_admin + end + + class Scope < ApplicationPolicy::Scope + def resolve + if user&.admin? + scope.all + elsif user_can_warehouse + scope.where(user: user) + else + scope.none + end + end + + private + + def user_can_warehouse + user&.can_warehouse? || user&.admin? + end + end +end diff --git a/app/policies/warehouse/sku_policy.rb b/app/policies/warehouse/sku_policy.rb new file mode 100644 index 0000000..5754bcc --- /dev/null +++ b/app/policies/warehouse/sku_policy.rb @@ -0,0 +1,23 @@ +class Warehouse::SKUPolicy < ApplicationPolicy + def index? + user_can_warehouse + end + def show? + user_can_warehouse + end + def create? + user_is_admin + end + def new? + user_is_admin + end + def update? + user_is_admin + end + class Scope < ApplicationPolicy::Scope + # NOTE: Be explicit about which records you allow access to! + # def resolve + # scope.all + # end + end +end diff --git a/app/policies/warehouse/template_policy.rb b/app/policies/warehouse/template_policy.rb new file mode 100644 index 0000000..25b1617 --- /dev/null +++ b/app/policies/warehouse/template_policy.rb @@ -0,0 +1,40 @@ +class Warehouse::TemplatePolicy < ApplicationPolicy + def index? + user_can_warehouse + end + + def show? + return true if user_is_admin + return false unless user_can_warehouse + record_belongs_to_user || record.public? + end + + def new? + user_can_warehouse + end + + def create? + user_can_warehouse + end + + def edit? + return true if user_is_admin + user_can_warehouse && record_belongs_to_user + end + + def update? + return true if user_is_admin + user_can_warehouse && record_belongs_to_user + end + + def destroy? + return true if user_is_admin + user_can_warehouse && record_belongs_to_user + end + + private + + def record_belongs_to_user + user && record.user == user + end +end diff --git a/app/services/ai_service.rb b/app/services/ai_service.rb new file mode 100644 index 0000000..9395e5c --- /dev/null +++ b/app/services/ai_service.rb @@ -0,0 +1,93 @@ +class AIService + class << self + def client + @client ||= OpenAI::Client.new + end + + def fix_address(row, field_mapping) + # Create a hash of the address fields for translation + address_data = { + "first_name" => row[field_mapping["first_name"]], + "last_name" => row[field_mapping["last_name"]], + "line_1" => row[field_mapping["line_1"]], + "line_2" => row[field_mapping["line_2"]], + "city" => row[field_mapping["city"]], + "state" => row[field_mapping["state"]], + "postal_code" => row[field_mapping["postal_code"]], + "country" => row[field_mapping["country"]], + } + + # Get translated address + translated = gptize_address(field_mapping.keys, address_data) + + # Return the translated fields, passing through email and phone directly + { + first_name: address_data["first_name"]&.presence, + last_name: address_data["last_name"]&.presence, + line_1: translated["line_1"]&.presence, + line_2: translated["line_2"]&.presence, + city: translated["city"]&.presence, + state: translated["state"]&.presence, + postal_code: translated["postal_code"]&.presence, + country: translated["country"]&.presence, + phone_number: row[field_mapping["phone_number"]]&.presence, + email: row[field_mapping["email"]]&.presence, + } + end + + private + + def gptize_address(fields, order) + # Filter out email and phone from fields to translate + address_fields = fields - ["email", "phone_number"] + + response = client.chat( + parameters: { + model: "gpt-4o-mini", + response_format: { + type: "json_schema", + json_schema: { + name: "postal_address", + schema: { + strict: true, + type: "object", + additionalProperties: false, + properties: { + line_1: { type: "string", description: "Street address only, no city/state/zip" }, + line_2: { type: "string", description: "Secondary address info (apt, suite, etc) or overflow from line_1 if needed" }, + city: { type: "string", description: "City name only, no state/zip" }, + state: { type: "string", description: "State/province/region code only" }, + postal_code: { type: "string", description: "Postal code only" }, + country: { type: "string", description: "ISO 3166-1 alpha-2 country code" }, + }, + required: ["line_1", "city", "state", "postal_code", "country"], + }, + }, + }, + messages: [{ + role: "user", + content: <<~PROMPT, + Please translate and format this address for international mail delivery: + 1. Translate to English using Latin characters + 2. Handle location information: + - If line_2 contains city/state (e.g., "Jebel Ali, Dubai"), move to proper fields + - For PO Box addresses, keep PO Box in line_1, remove location from line_2 + - Never leave city/state information in line_2 + 3. For country codes: + - Use ISO 3166-1 alpha-2 format (e.g., 'US', 'GB', 'JP') + - Convert localized names to codes (e.g., "MAGYARORSZÁG" -> "HU") + 4. Preserve special characters and formatting (building numbers, floor numbers) + 5. IMPORTANT: Never lose any information! If in doubt, keep it in the address + + Address to format: + #{address_fields.map { |field| order[field] && "#{field}: #{order[field]}" }.compact.join("\n")} + PROMPT + }], + temperature: 0.8, + }, + ) + + JSON.parse(response.dig("choices", 0, "message", "content")) + end + end +end diff --git a/app/services/customs_receipt.rb b/app/services/customs_receipt.rb new file mode 100644 index 0000000..3270631 --- /dev/null +++ b/app/services/customs_receipt.rb @@ -0,0 +1,48 @@ +module CustomsReceipt + class CustomsReceiptItem < Literal::Data + prop :name, String + prop :quantity, Integer + prop :value, _Any + end + + class CustomsReceiptable < Literal::Data + prop :order_number, String + prop :tracking_number, _String? + prop :carrier, _String? + prop :not_gifted, _Boolean, default: false + prop :additional_info, _String? + prop :contents, _Array(CustomsReceiptItem) + prop :shipping_cost, _Any? + prop :recipient_address, String + + def total_value + contents.sum { |item| item.quantity * item.value } + end + end + + def mock_receiptable + CustomsReceiptable.new( + order_number: "pkg!sdfjkls", + tracking_number: "9400182239154327", + not_gifted: false, + additional_info: "This package is nothing to worry about.", + contents: [ + CustomsReceiptItem.new( + name: "$30K in cash", + quantity: 3, + value: 30_000.99, + ), + CustomsReceiptItem.new( + name: "*mysterious ticking noise...*", + quantity: 1, + value: 29.95, + ), + ], + shipping_cost: 5.99, + recipient_address: "ATTN: Director of Commercial Payment\nUS Postal Service\n475 L’Enfant Plz SW Rm 3436\nWashington DC 20260-4110\nUSA", + carrier: "USPS", + ) + end + + module_function :mock_receiptable +end diff --git a/app/services/customs_receipt/generate.rb b/app/services/customs_receipt/generate.rb new file mode 100644 index 0000000..13873ee --- /dev/null +++ b/app/services/customs_receipt/generate.rb @@ -0,0 +1,193 @@ +module CustomsReceipt + module Generate + def run(receiptable) + options = { + process_timeout: 30, + browser_options: { + "no-sandbox" => nil, + }, + } + + options = options.merge(browser_path: "/usr/bin/chromium") if Rails.env.production? + + FerrumPdf.browser(**options) + + FerrumPdf.render_pdf( + html: CustomsReceiptTemplate.new(receiptable).call, + pdf_options: { + print_background: true, + }, + ) + end + + def write_mock_receipt + File.write("foo.html", CustomsReceiptTemplate.new(mock_receiptable).call) + end + + module_function :write_mock_receipt, :run + + class CustomsReceiptTemplate < Phlex::HTML + include ActiveSupport::NumberHelper + + def initialize(receiptable) + @r = receiptable + end + + def cell(align = :left, style = {}) + div class: "cell", align:, style: do + yield + end + end + + def row + div class: "row" do + yield + end + end + + def info(text = "") + div class: "info" do + plain text if text.present? + yield if block_given? + end + end + + def view_template + doctype + html do + head do + meta charset: "UTF-8" + style do + # + raw safe <<~CSS + body { + padding: 16px; + color: black; + font-family: sans-serif; + } + h4 { + font-weight: 400 + } + .table { + display: grid; + grid-template-columns: 1fr auto auto; + width: 100% + } + .table > .header { + display: contents; + } + .table .cell { + padding: .5px 8px + } + .table > .header > .cell { + background: #000; + color: #fff; + font-weight: 400; + text-align: left; + } + .table > .row { + display: contents; + } + .table > .row:nth-child(odd) * + { + background-color: #ececec; + } + td { + border: none; + } + img.emoji { + margin: 0 .05em 0 .1em; + vertical-align: -0.1em; + } + div.info { + margin-top: .54em; + } + .footer { + font-size: 10pt; + color: #999999; + position: fixed; + bottom: 0px; + width: 100%; + white-space: nowrap; + } + pre { + white-space: pre-wrap; + font-family: sans-serif; + margin-top: 4px; + } + CSS + end + title { "Customs Receipt – #{@r.order_number}" } + end + body do + img height: 64, style: { display: "inline-block", float: "left" }, src: HC_LOGO, class: "emoji" + div style: { display: "inline-block", padding_left: "10px" } do + b { "Hack Club" } + br + plain "15 Falls Rd " + br + plain "Shelburne, VT 05482 " + br + plain "United States" + end + br + br + br + b { "Shipment ID:" } + plain " " + @r.order_number + br + br + div style: { display: "inline-block" } do + b { "Ship to: " } + pre { @r.recipient_address } + end + br + br + b { "Contents:" } + div class: "table" do + div class: "header" do + cell { "Item" } + cell(:right) { "Quantity" } + cell(:right) { "Value/pc" } + end + @r.contents.each do |item| + row do + cell { item.name } + cell(:right) { item.quantity } + cell(:right) { number_to_currency item.value } + end + end + row do + cell(:right, grid_column: "span 3") do + "Total: #{number_to_currency @r.total_value}" + end + end + end + if @r.shipping_cost.present? + info "Shipping cost: #{number_to_currency(@r.shipping_cost)}" do + plain " via #{@r.carrier}" if @r.carrier.present? + end + end + if @r.tracking_number.present? + info "Original tracking number: #{@r.tracking_number}" + end + if @r.additional_info.present? + br + info raw(safe(@r.additional_info)) + end + info do + plain "All amounts are given in USD." + br + br + b { "N.B." } + plain "This shipment is a gift. Valuations are for customs purposes only." + end + p(class: "footer") { "Hack Club is a 501(c)(3) public charity. Our nonprofit EIN is 81-2908499." } + end + end + end + + HC_LOGO = "data:image/png;base64,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" + end + end +end diff --git a/app/services/customs_receipt/theseus_specific.rb b/app/services/customs_receipt/theseus_specific.rb new file mode 100644 index 0000000..6f32e9e --- /dev/null +++ b/app/services/customs_receipt/theseus_specific.rb @@ -0,0 +1,72 @@ +module CustomsReceipt + module TheseusSpecific + class << self + def receiptable_from_zenventory_order( + order:, + order_number:, + tracking_number:, + carrier:, + shipping_cost:, + not_gifted: false, + additional_info: nil + ) + CustomsReceiptable.new( + order_number:, + tracking_number:, + carrier:, + not_gifted: false, + additional_info:, + contents: order.dig(:items)&.map do |item| + price = item.dig(:price) + CustomsReceiptItem.new( + name: item.dig(:description), + quantity: item.dig(:quantity), + value: (price != 0.0 ? price : Warehouse::SKU.find_by(sku: item.dig(:sku))&.declared_unit_cost.to_f) || 1.0, + ) + end, + shipping_cost:, + recipient_address: [ + "#{order.dig(:shippingAddress, :name)}", + order.dig(:shippingAddress, :line1), + order.dig(:shippingAddress, :line2), + "#{order.dig(:shippingAddress, :city)}, #{order.dig(:shippingAddress, :state)} #{order.dig(:shippingAddress, :zip)}", + order.dig(:shippingAddress, :country), + ].compact_blank.join("\n"), + ) + end + + def receiptable_from_warehouse_order(order) + zenv = Zenventory.get_customer_order(order.zenventory_id) + additional_info = nil + + receiptable_from_zenventory_order( + order: zenv, + order_number: order.hc_id, + tracking_number: order.tracking_number, + carrier: order.carrier, + shipping_cost: order.postage_cost, + additional_info:, + ) + end + + def receiptable_from_msr(msr) + zenv = Zenventory.order_by_hc_id(msr.source_id) + additional_info = nil + + puts msr["Request Type"] + if msr["Request Type"]&.include? "High Seas" + additional_info = 'This shipment was sent out as part of High Seas, a grant program that rewards students for time spent programming.' + end + + receiptable_from_zenventory_order( + order: zenv, + order_number: msr.source_id, + tracking_number: msr["Warehouse–Tracking Number"], + carrier: msr["Warehouse–Service"], + shipping_cost: msr["Warehouse–Postage Cost"]&.to_f, + additional_info:, + ) + end + end + end +end diff --git a/app/services/easypost_service.rb b/app/services/easypost_service.rb new file mode 100644 index 0000000..442a3f5 --- /dev/null +++ b/app/services/easypost_service.rb @@ -0,0 +1,5 @@ +class EasyPostService + def self.client + @client ||= EasyPost::Client.new(api_key: Rails.application.credentials.dig(:easypost, :api_key)) + end +end diff --git a/app/services/flavor.rb b/app/services/flavor.rb new file mode 100644 index 0000000..b051b70 --- /dev/null +++ b/app/services/flavor.rb @@ -0,0 +1,32 @@ +class Flavor + class << self + def comma(name) + name && ", #{name}" + end + def time_based_greeting(name: nil) + current_hour = Time.now.hour + case current_hour + when 0..11 + "Good morning#{comma(name)}!" + when 12..17 + "Good afternoon#{comma(name)}!" + else + "Good evening#{comma(name)}..." + end + end + def greeting(name: nil) + [ + time_based_greeting(name:), + "despite everything, it's still #{name || 'you'}...", + "hey #{name || 'you'}!" + ].sample + end + def good_job_flavor + [ + "it ain't much, but it's honest work...", + "good enough for government work...", + "who up jobbing they good?" + ].sample + end + end +end diff --git a/app/services/geocoding_service.rb b/app/services/geocoding_service.rb new file mode 100644 index 0000000..496dfde --- /dev/null +++ b/app/services/geocoding_service.rb @@ -0,0 +1,91 @@ +module GeocodingService + FIFTEEN_FALLS = { place_id: 339396112, + licence: "Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright", + osm_type: "node", + osm_id: 12202788601, + lat: "44.3805465", + lon: "-73.2270890", + category: "office", + type: "ngo", + place_rank: 30, + importance: 5.165255207136671e-05, + addresstype: "office", + name: "Hack Club HQ", + display_name: "Hack Club HQ, 15, Falls Road, Shelburne Village Historic District, Shelburne, Chittenden County, Vermont, 05482, United States", + boundingbox: ["44.3804965", "44.3805965", "-73.2271390", "-73.2270390"] } + + class << self + def geocode_address_model(address, exact: false) + Rails.cache.fetch("geocode_address_#{address.id}", expires_in: 5.months) do + params = { + city: address.city, + state: address.state, + postalcode: address.postal_code, + country: address.country, + } + params[:street] = address.line_1 if exact + + first_hit(params) || FIFTEEN_FALLS + end + end + + def geocode_return_address(return_address, exact: false) + Rails.cache.fetch("geocode_return_address_#{return_address.id}", expires_in: 5.months) do + params = { + city: return_address.city, + state: return_address.state, + postalcode: return_address.postal_code, + country: return_address.country, + } + params[:street] = return_address.line_1 if exact + + first_hit(params) || FIFTEEN_FALLS + end + end + + def first_hit(params) + google_geocode(params) + end + + def google_geocode(params) + Rails.logger.info "Google Geocoding: #{params}" + + address_components = [] + address_components << params[:street] if params[:street] + address_components << params[:city] if params[:city] + address_components << params[:state] if params[:state] + address_components << params[:postalcode] if params[:postalcode] + address_components << params[:country] if params[:country] + + address = address_components.join(", ") + + response = conn.get("maps/api/geocode/json", { + address: address, + key: ENV["GOOGLE_MAPS_API_KEY"] + }) + + results = response.body["results"] + return nil if results.empty? + + result = results.first + location = result["geometry"]["location"] + + { + lat: location["lat"].to_s, + lon: location["lng"].to_s, + display_name: result["formatted_address"], + place_id: result["place_id"] + } + end + + private + + def conn + @conn ||= Faraday.new(url: "https://maps.googleapis.com/") do |faraday| + faraday.request :url_encoded + faraday.adapter Faraday.default_adapter + faraday.response :json + end + end + end +end diff --git a/app/services/geocoding_service/FACILITY.xlsx b/app/services/geocoding_service/FACILITY.xlsx new file mode 100644 index 0000000..779f31a Binary files /dev/null and b/app/services/geocoding_service/FACILITY.xlsx differ diff --git a/app/services/geocoding_service/README.txt b/app/services/geocoding_service/README.txt new file mode 100644 index 0000000..04fc048 --- /dev/null +++ b/app/services/geocoding_service/README.txt @@ -0,0 +1 @@ +update FACILITY.xlsx from https://postalpro.usps.com/node/1057 every so often \ No newline at end of file diff --git a/app/services/geocoding_service/usps_facilities.rb b/app/services/geocoding_service/usps_facilities.rb new file mode 100644 index 0000000..b060718 --- /dev/null +++ b/app/services/geocoding_service/usps_facilities.rb @@ -0,0 +1,50 @@ +module GeocodingService + class USPSFacilities + class << self + SPECIAL_CASES = { + "001376" => { + lat: "40.6532171", + lon: "-73.7712486", + }, + } + + def coords_for_locale_key(locale_key) + SPECIAL_CASES[locale_key] || + Rails.cache.fetch("geocode_usps_facility_#{locale_key}", expires_in: 1.day) do + GeocodingService.first_hit(address_for_locale_key(locale_key)) || + GeocodingService.first_hit(address_for_locale_key(locale_key).slice(:city, :state, :postalcode, :country)) + end + end + + def address_for_locale_key(locale_key) + facility = find_facility(locale_key) + return nil unless facility + + { + street: facility["FACILITY ADDRESS"], + city: facility["FACILITY CITY"], + state: facility["FACILITY STATE"], + postalcode: facility["ZIP"][0...5], + country: "US", + } + end + + def find_facility(locale_key) + facilities[locale_key] + end + + private + + def facilities + @facilities ||= load_facilities + end + + def load_facilities + Rails.logger.info "Loading USPS facilities" + facilities_file = File.join(File.dirname(__FILE__), "FACILITY.xlsx") + xsv = Xsv.open(facilities_file, parse_headers: true).first + return xsv.to_a.index_by { |row| row["LOCALE KEY"] } + end + end + end +end diff --git a/app/services/qz_tray_service.rb b/app/services/qz_tray_service.rb new file mode 100644 index 0000000..cb75a55 --- /dev/null +++ b/app/services/qz_tray_service.rb @@ -0,0 +1,17 @@ +class QZTrayService + class << self + def private_key + @private_key ||= OpenSSL::PKey.read(File.read(File.join(ENV["QZ_CERTS_PATH"], "private-key.pem")), ENV["QZ_PK_PASSWORD"]) + end + + def certificate + @cert ||= File.read(File.join(ENV["QZ_CERTS_PATH"], "digital-certificate.txt")) + end + + def sign(message) + digest = OpenSSL::Digest::SHA512.new + sig = private_key.sign(digest, message) + Base64.encode64(sig) + end + end +end diff --git a/app/services/tracking.rb b/app/services/tracking.rb new file mode 100644 index 0000000..cfecf72 --- /dev/null +++ b/app/services/tracking.rb @@ -0,0 +1,38 @@ +class Tracking + class << self + TRACKING_URL_FORMATS = { + asendia: "https://a1.asendiausa.com/tracking/?trackingnumber=%s", + usps: "https://tools.usps.com/go/TrackConfirmAction_input?strOrigTrackNum=%s", + ups: "https://www.ups.com/track?loc=en_US&tracknum=%s&requester=ST/trackdetails", + fedex: "https://www.fedex.com/fedextrack/?trknbr=%s", + generic: "https://parcelsapp.com/en/tracking/%s" + }.freeze + + def tracking_url_for(format, trknum) + return nil if !trknum || trknum.empty? # no tracking link for FCM(I) :-P + + TRACKING_URL_FORMATS[format] % trknum + end + + def get_format_by_zenv_info(carrier:, service:) + case carrier # figure out provider + when "Asendia" + :asendia + when "UPS", "UPS by ShipStation" # whyyyy + :ups + when /^Stamps\.com( \(Zenventory USPS\))?$/ + case service + when /GlobalPost .* Intl/ + :generic # these have better tracking than parcels at epgshipping.com but i can't figure out how to generate a URL to their portal + else + :usps # i think USPS is the only other carrier zenv uses SDC for? + end + when "FedEx" + :fedex + else + Rails.logger.warn("missing tracking format for carrier #{carrier}") + :generic # not a huge parcels fan but it'll catch everything else + end + end + end +end diff --git a/app/services/usps/api_service.rb b/app/services/usps/api_service.rb new file mode 100644 index 0000000..855683a --- /dev/null +++ b/app/services/usps/api_service.rb @@ -0,0 +1,273 @@ +module USPS + class USPSError < StandardError; end + + class NotFound < USPSError; end + + class NxAddress < NotFound; end + + class NxZIP < NotFound; end +end + +module FaradayMiddleware + # USPS' oauth is silly so i'm not bothering with proper refresh tokens + # we can just get a new client_credentials grant + class USPSRefresh < Faraday::Middleware + attr_reader :token + attr_reader :client + + def call(env) + if !@token || @token&.expired? + @token = client.client_credentials.get_token + end + env[:request_headers].merge!("Authorization" => "Bearer #{@token.token}") + @app.call(env) + end + + def initialize(app, client = nil) + super app + @app = app + @client = client + end + end + + class USPSErrorMiddleware < Faraday::Middleware + def on_complete(env) + unless env.response.success? + unless env.response.body.respond_to?(:dig) + uuid = Honeybadger.notify(USPS::USPSError.new(env.response.body)) + raise USPS::USPSError, "#{env.response.body} (please report EID: #{uuid})" + end + if env.response.body.dig(:error, :message) == "Address Not Found." + raise USPS::NxAddress + elsif env.response.body.dig(:error, :message) == "Invalid Zip Code." + raise USPS::NxZIP + else + uuid = Honeybadger.notify(USPS::USPSError.new(env.response.body)) + raise USPS::USPSError, "#{env.response.body} (please report EID: #{uuid})" + end + end + end + end +end + +Faraday::Request.register_middleware usps_refresh: FaradayMiddleware::USPSRefresh +Faraday::Response.register_middleware usps_error: FaradayMiddleware::USPSErrorMiddleware + +class USPS::APIService + ENVIRONMENT = Rails.env.production? ? :prod : :tem + + class << self + # Returns the best standardized address for a given address + # --- + # Standardizes street addresses including city and street abbreviations as well as providing missing information such as ZIP Code and ZIP+4. + # + # Must specify a street address, a state, and either a city or a ZIP Code. + # @param [String, nil] firm business name, helps USPS figure out suite numbers + # @param street_address address line 1 + # @param secondary_address apt/ste/what have you + # @param city take a wild guess + # @param state gotta be a 2-letter abbreviation! + # @param urbanization only in puerto rico..? + # @param zip_code zip code, provide this if you didn't provide city! + # @param zip_plus_4 zip+4, why would you be standardizing an address if you knew this? + # + # returns something in the shape of: + # {:firm=>"HACK CLUB", + # :address=> + # {:streetAddress=>"15 FALLS RD", + # :streetAddressAbbreviation=>nil, + # :secondaryAddress=>nil, + # :city=>"SHELBURNE", + # :cityAbbreviation=>nil, + # :state=>"VT", + # :postalCode=>nil, + # :province=>nil, + # :ZIPCode=>"05482", + # :ZIPPlus4=>"7480", + # :urbanization=>nil, + # :country=>nil, + # :countryISOCode=>nil}, + # :additionalInfo=>{:deliveryPoint=>"15", + # :carrierRoute=>"R003", + # :DPVConfirmation=>"Y", + # :DPVCMRA=>"N", + # :business=>"Y", + # :centralDeliveryPoint=>"N", + # :vacant=>"N"}, + # :corrections=>nil, + # :matches=>nil} + def standardize_address( + firm: nil, + street_address:, + secondary_address: nil, + city: nil, + state:, + urbanization: nil, + zip_code: nil, + zip_plus_4: nil + ) + conn.get("/addresses/v3/address", { + firm: firm, + streetAddress: street_address, + secondaryAddress: secondary_address, + city: city, + state: state, + urbanization: urbanization, + ZIPCode: zip_code, + ZIPPlus4: zip_plus_4, + }.compact_blank).body + end + + # Returns the city and state for a given ZIP Code. + # {:city=>"BURLINGTON", :state=>"VT", :ZIPCode=>"05401"} + # @param [String] zip zip code + def city_state_from_zip(zip) + conn.get("/addresses/v3/city-state", { ZIPCode: zip }).body + end + + # Returns the ZIP Code; and ZIP + 4; corresponding to the given address, city, and state (use USPS state abbreviations). + # @param [String] firm Firm/business corresponding to the address. + # @param [String] street_address The number of a building along with the name of the road or street on which it is located. + # @param [String] secondary_address The secondary unit designator, such as apartment(APT) or suite(STE) number, defining the exact location of the address within a building. For more information please see [Postal Explorer](https://pe.usps.com/text/pub28/28c2_003.htm). + # @param [String] city take a wild frickin guess + # @param [String] state capital two-character state code + # @param [String] zip_code why would you specify this + # @param [String] zip_plus_4 why on earth would you specify this + # + # {:firm=>nil, + # :address=> + # {:streetAddress=>"15 FALLS RD", + # :streetAddressAbbreviation=>nil, + # :secondaryAddress=>nil, + # :secondaryAddress=>nil, + # :city=>"SHELBURNE", + # :cityAbbreviation=>nil, + # :state=>"VT", + # :postalCode=>nil, + # :province=>nil, + # :ZIPCode=>"05482", + # :ZIPPlus4=>"7480", + # :urbanization=>nil, + # :country=>nil, + # :countryISOCode=>nil}} + def zip_code_for_address( + firm: nil, + street_address:, + secondary_address: nil, + city:, + state:, + zip_code: nil, + zip_plus_4: nil + ) + conn.get("/addresses/v3/zipcode", { + firm: firm, + streetAddress: street_address, + secondaryAddress: secondary_address, + city: city, + state: state, + ZIPCode: zip_code, + ZIPPlus4: zip_plus_4, + }.compact_blank).body + end + + # buys a piece of domestic first-class postage! + # + # @param [String] payment_token USPS payment token + # @param [String] processing_category processing category – "LETTERS" or "FLATS" + # @param [Float] weight weight of mailpiece including envelope (ounces) + # @param [Date] mailing_date (today->today+1 week) + # @param [Float] length (inches) + # @param [Float] height (inches) + # @param [Float] thickness (inches) + # @param non_machinable_indicators hash of {theOnesThatApply=>true} + # @param [String] receipt_option you want a receipt with that? specify "SEPARATE_PAGE" then + # @param [String] image_type "PDF"|"TIFF"|"JPG"|"SVG" + # @param [String] label_type "2X1.5LABEL" for now... + def create_fcm_indicia( + payment_token:, + processing_category:, + weight:, + mailing_date:, + length:, + height:, + thickness:, + non_machinable_indicators: { + isPolybagged: false, + hasClosureDevices: false, + hasLooseItems: false, + isRigid: false, + isSelfMailer: false, + isBooklet: false, + }, + receipt_option: "NONE", + image_type: "TIFF", + label_type: "2X1.5LABEL" + ) + conn.post( + "/labels/v3/indicia", + { + indiciaDescription: { + processingCategory: processing_category, + weight: weight, + mailingDate: mailing_date.to_s, + length: length, + height: height, + thickness: thickness, + nonMachinableIndicators: non_machinable_indicators, + }, + imageInfo: { + receiptOption: receipt_option, + imageType: image_type, + labelType: label_type, + }, + }, + { + "X-Payment-Authorization-Token" => payment_token, + "Accept" => "application/vnd.usps.labels+json", + }, + ).body + end + + # ugh i can't document this rn + # see https://developers.usps.com/paymentsv3#tag/Resources/operation/post-payments-payment-authorization + # @return [String] USPS v3 payment account token + def create_payment_token(roles:) + conn.post("/payments/v3/payment-authorization", { roles: }).body.dig(:paymentAuthorizationToken) + end + + def payment_account_inquiry(account_number:, account_type:, permit_zip: nil, amount: nil) + conn.get("/payments/v3/payment-account/#{account_number}", { accountType: account_type, permitZip: permit_zip, amount: }.compact_blank).body + end + + private + + def api_host + host = { + prod: "apis", + cat: "api-cat", + cat_no_s: "api-cat", + tem: "apis-tem", + }[ENVIRONMENT] + "https://#{host}.usps.com" + end + + def conn + @conn ||= Faraday.new(url: api_host) do |f| + f.request :usps_refresh, oauth2_client + f.request :json + f.response :usps_error + f.response :json, parser_options: { symbolize_names: true } + end + end + + def oauth2_client + @oa2_client ||= OAuth2::Client.new( + Rails.application.credentials.usps.dig(ENVIRONMENT, :consumer_id), + Rails.application.credentials.usps.dig(ENVIRONMENT, :consumer_secret), + site: "#{api_host}/oauth2/v3", + token_url: "token", + auth_scheme: :request_body, + ) + end + end +end diff --git a/app/services/usps/flirt_engine.rb b/app/services/usps/flirt_engine.rb new file mode 100644 index 0000000..376340d --- /dev/null +++ b/app/services/usps/flirt_engine.rb @@ -0,0 +1,243 @@ +module USPS + # First-Class Letter Inverse Rating Toolkit + class FLIRTEngine + class << self + # this will have to be updated when they come out with a new notice 123! + FCMI_RATE_TABLE = { + letter: { + 1.0 => { + ca: 1.65, + mx: 1.65, + other: 1.65 + }, + 2.0 => { + ca: 1.65, + mx: 2.50, + other: 2.98 + }, + 3.0 => { + ca: 2.36, + mx: 3.30, + other: 4.36 + }, + 3.5 => { + ca: 3.02, + mx: 4.14, + other: 5.75 + } + }, + flat: { + 1.0 => { + ca: 3.15, + mx: 3.15, + other: 3.15 + }, + 2.0 => { + ca: 3.55, + mx: 4.22, + other: 4.48 + }, + 3.0 => { + ca: 3.86, + mx: 5.16, + other: 5.78 + }, + 4.0 => { + ca: 4.12, + mx: 6.13, + other: 7.11 + }, + 5.0 => { + ca: 4.43, + mx: 7.09, + other: 8.41 + }, + 6.0 => { + ca: 4.73, + mx: 8.03, + other: 9.71 + }, + 7.0 => { + ca: 5.02, + mx: 9.01, + other: 11.01 + }, + 8.0 => { + ca: 5.32, + mx: 9.96, + other: 12.31 + }, + 12.0 => { + ca: 6.79, + mx: 12.03, + other: 14.92 + }, + 15.994 => { + ca: 8.27, + mx: 14.10, + other: 17.53 + } + } + } + FCMI_NON_MACHINABLE_SURCHARGE = 0.46 + + US_LETTER_RATES = { + 1.0 => 0.69, + 2.0 => 0.97, + 3.0 => 1.25, + 3.5 => 1.53 + } + + US_FLAT_RATES = { + 1.0 => 1.50, + 2.0 => 1.77, + 3.0 => 2.04, + 4.0 => 2.31, + 5.0 => 2.59, + 6.0 => 2.87, + 7.0 => 3.15, + 8.0 => 3.43, + 9.0 => 3.71, + 10.0 => 4.01, + 11.0 => 4.31, + 12.0 => 4.61, + 13.0 => 4.91 + } + + US_STAMP_LETTER_RATES = { + 1.0 => 0.73, + 2.0 => 1.01, + 3.0 => 1.29, + 3.5 => 1.57 + } + + US_STAMP_FLAT_RATES = { + 1.0 => 1.50, + 2.0 => 1.77, + 3.0 => 2.04, + 4.0 => 2.31, + 5.0 => 2.59, + 6.0 => 2.87, + 7.0 => 3.15, + 8.0 => 3.43, + 9.0 => 3.71, + 10.0 => 4.01, + 11.0 => 4.31, + 12.0 => 4.61, + 13.0 => 4.91 + } + + # calculate the retail FCMI price for a :letter or a :flat going to a given country + def desired_price(type, weight, country, non_machinable = false) + type = type.to_sym + rates = FCMI_RATE_TABLE[type] + + raise ArgumentError, "idk the rates for #{type}..." unless rates + country = case country + when "CA" + :ca + when "MX" + :mx + else + :other + end + + rate = rates.find { |k, v| weight <= k }&.dig(1) + raise "#{weight} oz is too heavy for an FCMI #{type}" unless rate + price = rate[country] + if non_machinable + raise ArgumentError, "only letters can be nonmachinable!" unless type == :letter + price += FCMI_NON_MACHINABLE_SURCHARGE + end + price + end + + # Calculate the metered rate for a US letter or flat + # @param type [Symbol] :letter or :flat + # @param weight [Float] weight in ounces + # @param non_machinable [Boolean] whether the item is non-machinable (only valid for letters) + # @return [Float] the metered rate price + def metered_price(type, weight, non_machinable = false) + type = type.to_sym + rates = case type + when :letter + US_LETTER_RATES + when :flat + US_FLAT_RATES + else + raise ArgumentError, "type must be :letter or :flat" + end + + rate = rates.find { |k, v| weight <= k }&.last + raise ArgumentError, "#{weight} oz is too heavy for a #{type}" unless rate + + if non_machinable + raise ArgumentError, "only letters can be non-machinable!" unless type == :letter + rate += FCMI_NON_MACHINABLE_SURCHARGE + end + + rate + end + + # Calculate the stamp rate for a US letter or flat + # @param type [Symbol] :letter or :flat + # @param weight [Float] weight in ounces + # @param non_machinable [Boolean] whether the item is non-machinable (only valid for letters) + # @return [Float] the stamp rate price + def stamp_price(type, weight, non_machinable = false) + type = type.to_sym + rates = case type + when :letter + US_STAMP_LETTER_RATES + when :flat + US_STAMP_FLAT_RATES + else + raise ArgumentError, "type must be :letter or :flat" + end + + rate = rates.find { |k, v| weight <= k }&.last + raise ArgumentError, "#{weight} oz is too heavy for a #{type}" unless rate + + if non_machinable + raise ArgumentError, "only letters can be non-machinable!" unless type == :letter + rate += FCMI_NON_MACHINABLE_SURCHARGE + end + + rate + end + + def closest_us_price(fcmi_rate) + best_option = nil + best_price = Float::INFINITY + + US_LETTER_RATES.each do |weight, price| + [ false, true ].each do |non_machinable| + adjusted_price = price + (non_machinable ? FCMI_NON_MACHINABLE_SURCHARGE : 0) + if adjusted_price >= fcmi_rate && adjusted_price < best_price + best_price = adjusted_price + best_option = { + processing_category: :letter, + weight: weight, + non_machinable: non_machinable + } + end + end + end + + US_FLAT_RATES.each do |weight, price| + if price >= fcmi_rate && price < best_price + best_price = price + best_option = { + processing_category: :flat, + weight: weight, + non_machinable: false + } + end + end + + raise ArgumentError, "can't figure out how to make $#{fcmi_rate} out of US rates, gotta use stamps instead :-(" unless best_option + best_option.merge(difference: best_price - fcmi_rate, price: best_price) + end + end + end +end diff --git a/app/services/usps/mc_nugget_engine.rb b/app/services/usps/mc_nugget_engine.rb new file mode 100644 index 0000000..7bfb9ed --- /dev/null +++ b/app/services/usps/mc_nugget_engine.rb @@ -0,0 +1,113 @@ +module USPS + class McNuggetEngine + class << self + # Common stamps that should be used first + COMMON_STAMPS = [ + { value: 0.73, name: "Forever" }, + { value: 1.65, name: "Global Forever" }, + { value: 1.19, name: "Non-machinable" }, + { value: 0.28, name: "Additional Ounce" }, + { value: 1.00, name: "$1" } + ].freeze + + # Less common stamps, ordered by value (largest first) + UNCOMMON_STAMPS = [ + { value: 0.40, name: "$0.40" }, + { value: 0.10, name: "$0.10" }, + { value: 0.05, name: "$0.05" }, + { value: 0.04, name: "$0.04" }, + { value: 0.03, name: "$0.03" }, + { value: 0.02, name: "$0.02" }, + { value: 0.01, name: "$0.01" } + ].freeze + + def find_stamp_combination(amount) + return {} unless amount + + remaining = amount.round(2) + combination = [] + + # Special case: if amount is a whole dollar, prefer $1 stamps + if remaining == remaining.floor + count = remaining.floor + return [ { name: "$1 stamp", count: count, value: 1.00 } ] if count > 0 + end + + # First try to use the largest possible stamp + all_stamps = (COMMON_STAMPS + UNCOMMON_STAMPS).sort_by { |s| -s[:value] } + + all_stamps.each do |stamp| + next if stamp[:value] > remaining + count = (remaining / stamp[:value]).floor + if count > 0 + count.times { combination << stamp } + remaining = (remaining - (stamp[:value] * count)).round(2) + end + end + + # If we couldn't make exact change, return nil + return nil if remaining > 0 + + # Group and count the stamps, then sort by count (descending) + grouped = combination.group_by { |s| s[:name] } + result = grouped.map do |name, stamps| + { name: "#{name} stamp", count: stamps.count, value: stamps.first[:value] } + end.sort_by { |s| -s[:count] } + + result + end + + def find_optimal_stamp_combination(amount) + return {} unless amount + + remaining = amount.round(2) + all_stamps = (COMMON_STAMPS + UNCOMMON_STAMPS).sort_by { |s| -s[:value] } + + # Initialize memoization table + memo = {} + + def self.min_stamps(amount, stamps, memo) + return [] if amount == 0 + return nil if amount < 0 + + # Check if we've already computed this amount + return memo[amount] if memo.key?(amount) + + best_combination = nil + min_count = Float::INFINITY + + stamps.each do |stamp| + next if stamp[:value] > amount + + # Try using this stamp + remaining = (amount - stamp[:value]).round(2) + sub_combination = min_stamps(remaining, stamps, memo) + + if sub_combination + current_combination = [ stamp ] + sub_combination + if current_combination.size < min_count + min_count = current_combination.size + best_combination = current_combination + end + end + end + + memo[amount] = best_combination + best_combination + end + + combination = min_stamps(remaining, all_stamps, memo) + + return nil if combination.nil? + + # Group and count the stamps, then sort by count (descending) + grouped = combination.group_by { |s| s[:name] } + result = grouped.map do |name, stamps| + { name: "#{name} stamp", count: stamps.count, value: stamps.first[:value] } + end.sort_by { |s| -s[:count] } + + result + end + end + end +end diff --git a/app/services/usps/pricing_engine.rb b/app/services/usps/pricing_engine.rb new file mode 100644 index 0000000..b5763e0 --- /dev/null +++ b/app/services/usps/pricing_engine.rb @@ -0,0 +1,206 @@ +module USPS + class PricingEngine + class << self + # this will have to be updated when they come out with a new notice 123! + FCMI_RATE_TABLE = { + letter: { + 1.0 => { + ca: 1.65, + mx: 1.65, + other: 1.65 + }, + 2.0 => { + ca: 1.65, + mx: 2.50, + other: 2.98 + }, + 3.0 => { + ca: 2.36, + mx: 3.30, + other: 4.36 + }, + 3.5 => { + ca: 3.02, + mx: 4.14, + other: 5.75 + } + }, + flat: { + 1.0 => { + ca: 3.15, + mx: 3.15, + other: 3.15 + }, + 2.0 => { + ca: 3.55, + mx: 4.22, + other: 4.48 + }, + 3.0 => { + ca: 3.86, + mx: 5.16, + other: 5.78 + }, + 4.0 => { + ca: 4.12, + mx: 6.13, + other: 7.11 + }, + 5.0 => { + ca: 4.43, + mx: 7.09, + other: 8.41 + }, + 6.0 => { + ca: 4.73, + mx: 8.03, + other: 9.71 + }, + 7.0 => { + ca: 5.02, + mx: 9.01, + other: 11.01 + }, + 8.0 => { + ca: 5.32, + mx: 9.96, + other: 12.31 + }, + 12.0 => { + ca: 6.79, + mx: 12.03, + other: 14.92 + }, + 15.994 => { + ca: 8.27, + mx: 14.10, + other: 17.53 + } + } + } + FCMI_NON_MACHINABLE_SURCHARGE = 0.46 + + US_LETTER_RATES = { + 1.0 => 0.69, + 2.0 => 0.97, + 3.0 => 1.25, + 3.5 => 1.53 + } + + US_FLAT_RATES = { + 1.0 => 1.50, + 2.0 => 1.77, + 3.0 => 2.04, + 4.0 => 2.31, + 5.0 => 2.59, + 6.0 => 2.87, + 7.0 => 3.15, + 8.0 => 3.43, + 9.0 => 3.71, + 10.0 => 4.01, + 11.0 => 4.31, + 12.0 => 4.61, + 13.0 => 4.91 + } + + US_STAMP_LETTER_RATES = { + 1.0 => 0.73, + 2.0 => 1.01, + 3.0 => 1.29, + 3.5 => 1.57 + } + + US_STAMP_FLAT_RATES = { + 1.0 => 1.50, + 2.0 => 1.77, + 3.0 => 2.04, + 4.0 => 2.31, + 5.0 => 2.59, + 6.0 => 2.87, + 7.0 => 3.15, + 8.0 => 3.43, + 9.0 => 3.71, + 10.0 => 4.01, + 11.0 => 4.31, + 12.0 => 4.61, + 13.0 => 4.91 + } + + def metered_price(processing_category, weight, non_machinable = false) + type = processing_category.to_sym + rates = case type + when :letter + US_LETTER_RATES + when :flat + US_FLAT_RATES + else + raise ArgumentError, "type must be :letter or :flat" + end + + rate = rates.find { |k, v| weight <= k }&.last + raise ArgumentError, "#{weight} oz is too heavy for a #{type}" unless rate + + if non_machinable + raise ArgumentError, "only letters can be non-machinable!" unless type == :letter + rate += FCMI_NON_MACHINABLE_SURCHARGE + end + + rate + end + + def domestic_stamp_price(processing_category, weight, non_machinable = false) + type = processing_category.to_sym + rates = case type + when :letter + US_STAMP_LETTER_RATES + when :flat + US_STAMP_FLAT_RATES + else + raise ArgumentError, "type must be :letter or :flat" + end + + rate = rates.find { |k, v| weight <= k }&.last + raise ArgumentError, "#{weight} oz is too heavy for a #{type}" unless rate + + if non_machinable + raise ArgumentError, "only letters can be non-machinable!" unless type == :letter + rate += FCMI_NON_MACHINABLE_SURCHARGE + end + + rate + end + + def fcmi_price(processing_category, weight, country, non_machinable = false) + type = processing_category.to_sym + rates = FCMI_RATE_TABLE[type] + + raise ArgumentError, "idk the rates for #{type}..." unless rates + country = case country + when "CA" + :ca + when "MX" + :mx + else + :other + end + + rate = rates.find { |k, v| weight <= k }&.dig(1) + raise "#{weight} oz is too heavy for an FCMI #{type}" unless rate + price = rate[country] + if non_machinable + raise ArgumentError, "only letters can be nonmachinable!" unless type == :letter + price += FCMI_NON_MACHINABLE_SURCHARGE + end + price + end + + def stamp_price(processing_category, weight, country, non_machinable = false) + if country == "US" + domestic_stamp_price(processing_category, weight, non_machinable) + else + fcmi_price(processing_category, weight, country, non_machinable) + end + end + end + end +end diff --git a/app/services/zenventory.rb b/app/services/zenventory.rb new file mode 100644 index 0000000..91a4053 --- /dev/null +++ b/app/services/zenventory.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +module FaradayMiddleware + class ZenventoryErrorMiddleware < Faraday::Middleware + def on_complete(env) + unless env.response.success? + Rails.logger.error("ZENV: #{env.response.body}") + if env.response.body&.dig(:code) == "invalid_parameter" + raise Zenventory::ZenventoryError, "invalid parameter: #{env.response.body.dig(:parameter)}" + end + raise Zenventory::ZenventoryError, env.response.body[:message] if env.response.body&.dig(:message) + raise Zenventory::ZenventoryError, "not found :-(" if env.response.status == 404 + raise Zenventory::ZenventoryError, "you're trying to create an order that already exists!" if env.response.body == '{"code":"invalid_parameter","message":"Invalid value for parameter.","parameter":"orderNumber"}' + raise Zenventory::ZenventoryError, "#{env.response.status}: #{env.response.body}" + end + end + end +end + +Faraday::Response.register_middleware zenventory_error: FaradayMiddleware::ZenventoryErrorMiddleware + +class Zenventory + class << self + def order_by_hc_id(id) + get_customer_orders(orderNumber: id, no_paginate: true).first + end + + def get_customer_orders(params = {}) + paginated_get("customer-orders", :customerOrders, params) + end + + def get_customer_order(id) + response = conn.get("customer-orders/#{id}").body + response + end + + def create_customer_order(params = {}) + conn.post("customer-orders", + saveAsDraft: !(ENV["SEND_REAL_ORDERS"] == "yeah"), + **params).body + end + + def update_customer_order(id, params = {}) + conn.put("customer-orders/#{id}", + saveAsDraft: !(ENV["SEND_REAL_ORDERS"] == "yeah"), + **params).body + end + + def get_inventory(params = {}) + paginated_get("inventory", :inventory, params) + end + + def get_kit_inventory(params = {}) + paginated_get("inventory/kits", :inventory, params) + end + + def get_items(params = {}) + paginated_get("items", :items, params) + end + + def get_item(id, include_units: false, include_bom: false) + conn.get("items/#{id}", includeUnits: include_units, includeBom: include_bom).body + end + + def get_purchase_orders(params = {}) + paginated_get("purchase-orders", :purchaseOrders, params) + end + + def get_purchase_order(id) + conn.get("purchase-orders/#{id}").body + end + + def run_report(category, report_key, params = {}) + CSV.parse( + conn.get("reports/#{category}/#{report_key}", csv: true, **params).body, + headers: true, + converters: %i[numeric date], + header_converters: %i[downcase symbol], + )&.map(&:to_h) + end + + def cancel_customer_order(id, reason) + resp = legacy_conn.put("customerorders/#{id}/cancel?reason=#{reason}").body + raise ZenventoryError, "couldn't cancel!" unless resp[:success] + end + + # ─── ・ 。゚☆: *.☽ .* :☆゚. ─── + + def conn + raise "zenventory: no creds?" unless Rails.application.credentials.dig(:zenventory, :api_key) && Rails.application.credentials.dig(:zenventory, :api_secret) + @conn ||= Faraday.new url: "https://app.zenventory.com/rest/" do |c| + c.request :json + c.request :authorization, :basic, Rails.application.credentials.dig(:zenventory, :api_key), Rails.application.credentials.dig(:zenventory, :api_secret) + c.response :zenventory_error + c.response :json, parser_options: { symbolize_names: true } + end + end + + def paginated_get(url, obj, params = {}, page_size: 100) + no_paginate = params.delete :no_paginate + page = 1 + data = [] + loop do + response = conn.get(url, page:, perPage: page_size, **params).body + data.concat(response[obj] || []) + page += 1 + break if page > (response.dig(:meta, :totalPages) || 0) or no_paginate + end + data + end + + def legacy_conn + @legacy_conn ||= Faraday.new(url: "https://app.zenventory.com/services/rest") do |c| + c.request :json + c.response :zenventory_error + c.response :json, parser_options: { symbolize_names: true } + c.headers["SecureKey"] = Rails.application.credentials.dig(:zenventory, :legacy_secure_key) || (raise "zenv: no legacy secure key set!") + end + end + end + + class ZenventoryError < StandardError; end +end diff --git a/app/views/addresses/_address.html.erb b/app/views/addresses/_address.html.erb new file mode 100644 index 0000000..9917e60 --- /dev/null +++ b/app/views/addresses/_address.html.erb @@ -0,0 +1,5 @@ +<%= address.first_name %> <%= address.last_name %>
+<%= address.line_1 %>
+<% if address.line_2.present? %> <%= address.line_2 %>
<% end %> +<%= address.city %>, <%= address.state %> <%= address.postal_code %>
+<%= address.country %> \ No newline at end of file diff --git a/app/views/addresses/_nested_form.html.erb b/app/views/addresses/_nested_form.html.erb new file mode 100644 index 0000000..575cb13 --- /dev/null +++ b/app/views/addresses/_nested_form.html.erb @@ -0,0 +1,48 @@ +<%= form.fields_for :address do |a| %> +
+
+ <%= a.label :first_name, "First Name", class: "form-label" %> + <%= a.text_field :first_name, class: "form-control" %> +
+
+ <%= a.label :last_name, "Last Name", class: "form-label" %> + <%= a.text_field :last_name, class: "form-control" %> +
+
+ +
+ <%= a.label :line_1, "Street Address", class: "form-label" %> + <%= a.text_field :line_1, class: "form-control" %> +
+ +
+ <%= a.label :line_2, "Apartment, Suite, etc.", class: "form-label" %> + <%= a.text_field :line_2, class: "form-control" %> +
Optional
+
+ +
+
+ <%= a.label :city, class: "form-label" %> + <%= a.text_field :city, class: "form-control" %> +
+ +
+ <%= a.label :state, "State/Province", class: "form-label" %> + <%= a.text_field :state, class: "form-control" %> +
+ +
+ <%= a.label :postal_code, "Postal/ZIP Code", class: "form-label" %> + <%= a.text_field :postal_code, class: "form-control" %> +
+
+ +
+ <%= a.label :country, "Country", class: "form-label" %> + <%= a.collection_select :country, Address.countries_for_select, + :first, :last, + { include_blank: "Select a country" }, + { class: "form-control" } %> +
+<% end %> \ No newline at end of file diff --git a/app/views/admin/users/show.html.erb b/app/views/admin/users/show.html.erb new file mode 100644 index 0000000..d631d99 --- /dev/null +++ b/app/views/admin/users/show.html.erb @@ -0,0 +1,56 @@ +<%# +# Show + +This view is the template for the show page. +It renders the attributes of a resource, +as well as a link to its edit page. + +## Local variables: + +- `page`: + An instance of [Administrate::Page::Show][1]. + Contains methods for accessing the resource to be displayed on the page, + as well as helpers for describing how each attribute of the resource + should be displayed. + +[1]: http://www.rubydoc.info/gems/administrate/Administrate/Page/Show +%> +<% content_for(:title) { t("administrate.actions.show_resource", name: page.page_title) } %> +
+

+ <%= content_for(:title) %> +

+
+ <%= link_to( + "impersonate 🥸", + impersonate_user_path(page.resource), + class: "button button--secondary", + ) if current_user&.admin? && current_user != page.resource %> + <%= link_to( + t("administrate.actions.edit_resource", name: page.page_title), + [:edit, namespace, page.resource], + class: "button", + ) if accessible_action?(page.resource, :edit) %> + <%= link_to( + t("administrate.actions.destroy"), + [namespace, page.resource], + class: "button button--danger", + method: :delete, + data: { confirm: t("administrate.actions.confirm") } + ) if accessible_action?(page.resource, :destroy) %> +
+
+
+
+ <% page.attributes.each do |attribute| %> +
+ <%= t( + "helpers.label.#{resource_name}.#{attribute.name}", + default: page.resource.class.human_attribute_name(attribute.name), + ) %> +
+
<%= render_field attribute, page: page %>
+ <% end %> +
+
diff --git a/app/views/api/v1/addresses/_address.json.jb b/app/views/api/v1/addresses/_address.json.jb new file mode 100644 index 0000000..1b9b4eb --- /dev/null +++ b/app/views/api/v1/addresses/_address.json.jb @@ -0,0 +1,16 @@ +json = { + first_name: address.first_name, + last_name: address.last_name, +} + +pii do + json[:line_1] = address.line_1 + json[:line_2] = address.line_2 + json[:city] = address.city + json[:state] = address.state + json[:postal_code] = address.postal_code + json[:country] = address.country + json[:phone_number] = address.phone_number +end + +json.compact_blank diff --git a/app/views/api/v1/letter_queues/create_letter.json.jb b/app/views/api/v1/letter_queues/create_letter.json.jb new file mode 100644 index 0000000..d4f8240 --- /dev/null +++ b/app/views/api/v1/letter_queues/create_letter.json.jb @@ -0,0 +1 @@ +render @letter diff --git a/app/views/api/v1/letter_queues/queued.json.jb b/app/views/api/v1/letter_queues/queued.json.jb new file mode 100644 index 0000000..f1a56f7 --- /dev/null +++ b/app/views/api/v1/letter_queues/queued.json.jb @@ -0,0 +1 @@ +@letters.map { |l| render l } diff --git a/app/views/api/v1/letters/_letter.json.jb b/app/views/api/v1/letters/_letter.json.jb new file mode 100644 index 0000000..9063949 --- /dev/null +++ b/app/views/api/v1/letters/_letter.json.jb @@ -0,0 +1,13 @@ +return_address = render letter.return_address +return_address[:name] = letter.return_address_name_line +{ + id: letter.public_id, + sender: letter.user.public_id, + status: letter.aasm_state, + tags: letter.tags, + return_address:, + address: render(letter.address), + label_url: pii { if_expand(:label) { rails_blob_url(letter.label) if letter.label.attached? } }, + rubber_stamps: letter.rubber_stamps, + metadata: letter.metadata || {}, +}.compact_blank diff --git a/app/views/api/v1/letters/show.json.jb b/app/views/api/v1/letters/show.json.jb new file mode 100644 index 0000000..5e0b52a --- /dev/null +++ b/app/views/api/v1/letters/show.json.jb @@ -0,0 +1,3 @@ +{ + letter: render(@letter), +} diff --git a/app/views/api/v1/return_addresses/_return_address.json.jb b/app/views/api/v1/return_addresses/_return_address.json.jb new file mode 100644 index 0000000..022e474 --- /dev/null +++ b/app/views/api/v1/return_addresses/_return_address.json.jb @@ -0,0 +1,8 @@ +{ + line_1: return_address.line_1, + line_2: return_address.line_2, + city: return_address.city, + state: return_address.state, + postal_code: return_address.postal_code, + country: return_address.country, +}.compact_blank diff --git a/app/views/api/v1/tags/index.json.jb b/app/views/api/v1/tags/index.json.jb new file mode 100644 index 0000000..eb9b55d --- /dev/null +++ b/app/views/api/v1/tags/index.json.jb @@ -0,0 +1,3 @@ +{ + tags: @tags, +} diff --git a/app/views/api/v1/tags/letters.json.jb b/app/views/api/v1/tags/letters.json.jb new file mode 100644 index 0000000..093547a --- /dev/null +++ b/app/views/api/v1/tags/letters.json.jb @@ -0,0 +1,3 @@ +{ + letters: render(@letters) || [], +} diff --git a/app/views/api/v1/tags/show.json.jb b/app/views/api/v1/tags/show.json.jb new file mode 100644 index 0000000..5a973ef --- /dev/null +++ b/app/views/api/v1/tags/show.json.jb @@ -0,0 +1,14 @@ +{ + tag: @tag, + letters: { + count: @letter_count, + postage_cost: @letter_postage_cost, + }, + warehouse_orders: { + count: @warehouse_order_count, + postage_cost: @warehouse_order_postage_cost, + labor_cost: @warehouse_order_labor_cost, + contents_cost: @warehouse_order_contents_cost, + total_cost: @warehouse_order_total_cost, + }, +} diff --git a/app/views/api/v1/users/_user.json.jb b/app/views/api/v1/users/_user.json.jb new file mode 100644 index 0000000..f950ce0 --- /dev/null +++ b/app/views/api/v1/users/_user.json.jb @@ -0,0 +1,7 @@ +{ + id: user.public_id, + name: user.username, + email: user.email, + avatar: user.icon_url, + admin: user.admin?, +} diff --git a/app/views/api/v1/users/show.json.jb b/app/views/api/v1/users/show.json.jb new file mode 100644 index 0000000..dfaa0f8 --- /dev/null +++ b/app/views/api/v1/users/show.json.jb @@ -0,0 +1 @@ +render @user diff --git a/app/views/api_keys/_api_key.html.erb b/app/views/api_keys/_api_key.html.erb new file mode 100644 index 0000000..ae866c7 --- /dev/null +++ b/app/views/api_keys/_api_key.html.erb @@ -0,0 +1,19 @@ +
+

+ <%= link_to api_key do %><%= api_key.pretty_name %> <% end %> + <% if api_key.active? %> +
+ active +
+ <% else %> +
+ revoked +
+ <% end %> +

+ <%= render 'shared/user_mention', user: api_key.user %> + PII: <%= render_checkbox api_key.pii %> + <% if api_key.may_impersonate? %>
+ impersonate: <%= render_checkbox true %> + <% end %> +
\ No newline at end of file diff --git a/app/views/api_keys/index.html.erb b/app/views/api_keys/index.html.erb new file mode 100644 index 0000000..a42a811 --- /dev/null +++ b/app/views/api_keys/index.html.erb @@ -0,0 +1,8 @@ +<% content_for :title do %> + API keys +<% end %> +

API keys

+
+ <%= create_button new_api_key_path, "visit the locksmith!" %> +
+<%= render @api_keys %> \ No newline at end of file diff --git a/app/views/api_keys/new.html.erb b/app/views/api_keys/new.html.erb new file mode 100644 index 0000000..557a1db --- /dev/null +++ b/app/views/api_keys/new.html.erb @@ -0,0 +1,30 @@ +

New API key

+<%= form_with model: @api_key do |f| %> +
+ <%= f.label :name, "Name" %> + <%= f.text_field :name %> + short description of what it's for – think "high-seas" +
+
+ <%= f.label :pii, "PII access?" %> + <%= f.check_box :pii %> + +
+ <% admin_tool do %> +
+ <%= f.label :may_impersonate, "Can impersonate?" %> + <%= f.check_box :may_impersonate %> + +
+ <% end %> + <%= f.submit "create API key!", class: "btn success btn-small" %> +<% end %> \ No newline at end of file diff --git a/app/views/api_keys/revoke_confirm.html.erb b/app/views/api_keys/revoke_confirm.html.erb new file mode 100644 index 0000000..7801e8f --- /dev/null +++ b/app/views/api_keys/revoke_confirm.html.erb @@ -0,0 +1,3 @@ +

Revoking key <%= @api_key.pretty_name %>

+

are you sure?
everything that depends on the key <%= @api_key.abbreviated %> will stop working and can't be reactivated.

+<%= button_to("do it. pull the trigger.", revoke_api_key_path(@api_key), class: 'btn danger') %> \ No newline at end of file diff --git a/app/views/api_keys/show.html.erb b/app/views/api_keys/show.html.erb new file mode 100644 index 0000000..11b05c2 --- /dev/null +++ b/app/views/api_keys/show.html.erb @@ -0,0 +1,47 @@ +

API key <%= @api_key.pretty_name %>:

+
+
+
+ key (click to copy): + <%= copy_to_clipboard(@api_key.token, tooltip_direction: 'e') do %> + <%= @api_key.abbreviated %> + <% end %> +
+<% unless @api_key.revoked? %> + <%= link_to "revoke", revoke_confirm_api_key_path(@api_key), class: "btn danger btn-small" %> +<% end %> +
+
+ status: + <% if @api_key.active? %> +
+ active +
+ <% else %> +
+ revoked +
+ <% end %> +
+
+
+ acts as: + <%= render 'shared/user_mention', user: @api_key.user %> +
+
+
+ sees PII? <%= render_checkbox @api_key.pii %> +
+<% if @api_key.may_impersonate? %> +
+ can impersonate? <%= render_checkbox true %> +
+<% end %> +
+
+ created: <%= @api_key.created_at.in_time_zone("America/New_York") %> + <% if @api_key.revoked? %> +
+ revoked: <%= @api_key.revoked_at.in_time_zone("America/New_York") %> + <% end %> +
\ No newline at end of file diff --git a/app/views/application/_admin_inspector.erb b/app/views/application/_admin_inspector.erb new file mode 100644 index 0000000..933c5df --- /dev/null +++ b/app/views/application/_admin_inspector.erb @@ -0,0 +1,36 @@ +<% admin_tool do %> +
+ + Inspect "<%= record.class.name.underscore %>" record + + + <% unless record.nil? %> + <% if record.is_a?(Norairrecord::Table) %> + <%== ap record.fields%> + <% else %> + <%== ap record %> + <% end %> +
+ + +
+ + View JSON + +
+
+<%= JSON.pretty_generate(record.as_json) %>
+            
+
+
+
+ + <% else %> + +

+ nil (does not exist) +

+ + <% end %> +
+<% end %> diff --git a/app/views/application/_line_item_fields.erb b/app/views/application/_line_item_fields.erb new file mode 100644 index 0000000..aef584c --- /dev/null +++ b/app/views/application/_line_item_fields.erb @@ -0,0 +1,28 @@ +
+ <% skus_by_category = Warehouse::SKU.in_inventory.order(:sku).group_by(&:category) %> + +
+
+ <%= f.grouped_collection_select :sku_id, + skus_by_category, + :last, + lambda { |group| group.first.humanize }, + :id, + -> (sku) { "#{sku.sku} - #{sku.name} (#{sku.in_stock} in stock)"}, + { include_blank: 'Select a SKU' }, + { class: 'needs-select2', disabled: f.object.persisted? } %> +
+ +
+ <%= f.number_field :quantity, class: 'form-control', min: 1, placeholder: 'Quantity' %> +
+ +
+ <%= link_to_remove_association "Remove", f, class: 'secondary outline' %> +
+
+ + <% if f.object.persisted? %> + <%= f.hidden_field :sku_id %> + <% end %> +
\ No newline at end of file diff --git a/app/views/base.rb b/app/views/base.rb new file mode 100644 index 0000000..0e4f1b3 --- /dev/null +++ b/app/views/base.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Views::Base < Components::Base + # The `Views::Base` is an abstract class for all your views. + + # By default, it inherits from `Components::Base`, but you + # can change that to `Phlex::HTML` if you want to keep views and + # components independent. +end diff --git a/app/views/batches/_addresses_table.html.erb b/app/views/batches/_addresses_table.html.erb new file mode 100644 index 0000000..6e003bd --- /dev/null +++ b/app/views/batches/_addresses_table.html.erb @@ -0,0 +1,33 @@ +
+ + + + + + + + + + + + + <% addresses.each do |address| %> + + + + + + + + + <% end %> + +
NameAddressCityStatePostal CodeCountry
+ <%= "#{address.first_name} #{address.last_name}" %> + + <%= address.line_1 %> + <% if address.line_2.present? %> +
<%= address.line_2 %> + <% end %> +
<%= address.city %><%= address.state %><%= address.postal_code %><%= address.country %>
+
\ No newline at end of file diff --git a/app/views/batches/_batch.html.erb b/app/views/batches/_batch.html.erb new file mode 100644 index 0000000..3ea9645 --- /dev/null +++ b/app/views/batches/_batch.html.erb @@ -0,0 +1,79 @@ +
+
+
+
+

<%= batch.type.split('::').first.titleize %> Batch #<%= batch.id %><%= batch_status_badge(batch.aasm.current_state) %>

+
+ +
+ <%= render 'shared/tags', tags: batch.tags %> + +
+
+

Batch Information

+
+

Created: <%= batch.created_at.strftime("%B %d, %Y at %I:%M %p") %>

+

CSV File: <%= batch.csv.filename if batch.csv.attached? %>

+

Address Count: <%= batch.addresses.count %>

+ <% if batch.is_a?(Letter::Batch) %> +

Letter Dimensions: <%= "#{batch.letter_width}\" × #{batch.letter_height}\"" %>

+

Letter Weight: <%= batch.letter_weight %> oz

+ <% end %> + <% if batch.is_a?(Warehouse::Batch) && batch.warehouse_template.present? %> +

Warehouse Template: <%= batch.warehouse_template.name %>

+ <% end %> +
+
+ +
+

Cost Information

+
+ <% if batch.is_a?(Warehouse::Batch) %> +

Contents Cost: $<%= sprintf('%.2f', batch.contents_cost) %>

+

Labor Cost: $<%= sprintf('%.2f', batch.labor_cost) %>

+

Shipping Cost: $<%= sprintf('%.2f', batch.postage_cost) %>

+ <% end %> +

Total Cost: $<%= sprintf('%.2f', batch.total_cost) %>

+
+
+
+ + <% if batch.is_a?(Letter::Batch) && batch.labels_pdf.attached? %> +
+

Letter Labels

+
+

PDF labels are available for this batch.

+ <%= primary_outline_link_to "Download Labels PDF", rails_blob_path(batch.labels_pdf, disposition: "attachment"), target: "_blank", class: "mt-2" do %> + <%= download_icon %>Download Labels PDF + <% end %> +
+
+ <% end %> +
+
+
+ +
+
+
+

Actions

+
+ +
+ <% case batch.aasm_state %> + <% when "awaiting_field_mapping" %> + <%= warning_link_to "Map Fields", map_fields_batch_path(batch), class: "w-full mb-2" do %> + + + + Map Fields + <% end %> + <% when "fields_mapped" %> + <%= success_link_to "Process Batch", process_confirm_batch_path(batch), class: "w-full" do %> + <%= check_icon %>Process Batch + <% end %> + <% end %> +
+
+
+
diff --git a/app/views/batches/_batch.json.jbuilder b/app/views/batches/_batch.json.jbuilder new file mode 100644 index 0000000..759813f --- /dev/null +++ b/app/views/batches/_batch.json.jbuilder @@ -0,0 +1,3 @@ +json.extract! batch, :id, :created_at, :updated_at +json.url batch_url(batch, format: :json) +json.csv_filename batch.csv.filename diff --git a/app/views/batches/_form.html.erb b/app/views/batches/_form.html.erb new file mode 100644 index 0000000..542d04e --- /dev/null +++ b/app/views/batches/_form.html.erb @@ -0,0 +1,19 @@ +<%= form_with(model: batch) do |form| %> + <% if batch.errors.any? %> +
+

<%= pluralize(batch.errors.count, "error") %> prohibited this batch from being saved:

+ + +
+ <% end %> + <%= form.label :csv, "upload a CSV:" %> + + <%= form.file_field :csv, accept: 'text/csv' %> +
+ <%= form.submit "onward!" %> +
+<% end %> diff --git a/app/views/batches/_letter_batch.html.erb b/app/views/batches/_letter_batch.html.erb new file mode 100644 index 0000000..cb069a4 --- /dev/null +++ b/app/views/batches/_letter_batch.html.erb @@ -0,0 +1,110 @@ +
+
+
+

Letter Batch Preview

+
+ +
+
+
+

From

+ <% if batch.letter_return_address.present? %> +
+
<%= batch.letter_return_address.name %>
+
<%= batch.letter_return_address.line_1 %>
+ <% if batch.letter_return_address.line_2.present? %> +
<%= batch.letter_return_address.line_2 %>
+ <% end %> +
<%= batch.letter_return_address.city %>, <%= batch.letter_return_address.state %> <%= batch.letter_return_address.postal_code %>
+
<%= batch.letter_return_address.country %>
+
+ <% else %> +
No return address
+ <% end %> +
+ +
+

Batch Information

+
+
Dimensions: <%= batch.letter_width %> × <%= batch.letter_height %> in
+
Weight: <%= batch.letter_weight %> oz
+
Mailer ID: <%= batch.mailer_id&.display_name %>
+
Address Count: <%= batch.addresses.count %>
+
+
+
+ + <% if batch.pdf_label.attached? %> +
+

Generated Labels

+
+
+
+

Labels PDF

+

Generated on <%= batch.pdf_label.created_at.strftime("%B %d, %Y at %I:%M %p") %>

+
+
+ <%= link_to rails_blob_path(batch.pdf_label, disposition: "attachment"), class: "btn btn-sm btn-primary" do %> + + + + Download PDF + <% end %> + + <% if batch.letters.any? && batch.letters.all?(&:pending?) %> + <%= button_to mark_printed_batch_path(batch), method: :post, class: "btn btn-sm btn-success", data: { confirm: "Are you sure you want to mark all letters as printed?" } do %> + + + + Mark All Printed + <% end %> + <% end %> + + <% if batch.letters.any? && batch.letters.all?(&:printed?) %> + <%= button_to mark_mailed_batch_path(batch), method: :post, class: "btn btn-sm btn-info", data: { confirm: "Are you sure you want to mark all letters as mailed?" } do %> + + + + Mark All Mailed + <% end %> + <% end %> +
+
+
+
+ <% else %> +
+

No labels have been generated yet.

+
+ <% end %> +
+
+
+ + \ No newline at end of file diff --git a/app/views/batches/_letters_collection.html.erb b/app/views/batches/_letters_collection.html.erb new file mode 100644 index 0000000..37ed61d --- /dev/null +++ b/app/views/batches/_letters_collection.html.erb @@ -0,0 +1,31 @@ +<% letters.each do |letter| %> +
+
+

<%= link_to letter.public_id, letter %> to <%= letter.address.name_line %> + <%= letter_status_badge(letter.aasm_state, 'float-right') %> +

+
+ +
+
+ From: + <%= letter.return_address.present? ? letter.return_address_name_line : 'N/A' %> +
+ +
+ Created: + <%= time_ago_in_words(letter.created_at) %> ago +
+ +
+ Size: + <%= letter.height %>" × <%= letter.width %>" +
+
+ +
+ Tags: + <%= render 'shared/tags', tags: letter.tags %> +
+
+<% end %> diff --git a/app/views/batches/_orders_collection.html.erb b/app/views/batches/_orders_collection.html.erb new file mode 100644 index 0000000..517fa36 --- /dev/null +++ b/app/views/batches/_orders_collection.html.erb @@ -0,0 +1,26 @@ +<% orders.each do |order| %> +
+
+

+ <%= link_to order.hc_id, order %> – <%= order.user_facing_title %> + <%= render 'warehouse/orders/status_badge', order: order %> +

+
+
+
+
+ Recipient: + <%= order.address.name_line %> (<%= order.recipient_email %>) +
+
+ Tags: + <%= render 'shared/tags', tags: order.tags %> +
+
+
+ Contents: + <%= render 'warehouse/orders/line_items', order: order %> +
+
+
+<% end %> diff --git a/app/views/batches/edit.html.erb b/app/views/batches/edit.html.erb new file mode 100644 index 0000000..eb8dcab --- /dev/null +++ b/app/views/batches/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing batch" %> + +

Editing batch

+ +<%= render "form", batch: @batch %> + +
+ +
+ <%= link_to "Show this batch", @batch %> | + <%= link_to "Back to batches", batches_path %> +
diff --git a/app/views/batches/import.html.erb b/app/views/batches/import.html.erb new file mode 100644 index 0000000..6505949 --- /dev/null +++ b/app/views/batches/import.html.erb @@ -0,0 +1,2 @@ +

Batches#import

+

Find me in app/views/batches/import.html.erb

diff --git a/app/views/batches/index.html.erb b/app/views/batches/index.html.erb new file mode 100644 index 0000000..bd6fb34 --- /dev/null +++ b/app/views/batches/index.html.erb @@ -0,0 +1,48 @@ +<% content_for :title, "Batches" %> +
+ +
+ <% @batches.each do |batch| %> +
+
+
+

+ <%= batch.type.split('::').first.titleize %> Batch #<%= batch.id %> + <%= batch_status_badge(batch.aasm.current_state, "float-right") %> +

+
+
+
+ Tags: + <%= render 'shared/tags', tags: batch.tags %> +
+
+
+ Type: + <%= batch.type.split('::').first.titleize %> +
+
+ Created: + <%= time_ago_in_words(batch.created_at) %> ago +
+
+ Status: + <%= batch.aasm.current_state.to_s.humanize(capitalize: false) %> +
+
+
+ <%= primary_outline_link_to "View Details", batch do %> + <%= eye_icon %>View Details + <% end %> +
+
+ <% end %> +
+
diff --git a/app/views/batches/index.json.jbuilder b/app/views/batches/index.json.jbuilder new file mode 100644 index 0000000..53afeec --- /dev/null +++ b/app/views/batches/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @batches, partial: "batches/batch", as: :batch diff --git a/app/views/batches/map.html.erb b/app/views/batches/map.html.erb new file mode 100644 index 0000000..e69de29 diff --git a/app/views/batches/map_fields.html.erb b/app/views/batches/map_fields.html.erb new file mode 100644 index 0000000..0118411 --- /dev/null +++ b/app/views/batches/map_fields.html.erb @@ -0,0 +1,232 @@ +

Map CSV Fields to Address Fields

+ +
+ <%= form_with(model: @batch, url: set_mapping_batch_path(@batch), method: :post) do |f| %> + <%# Define default_mapping outside of the loop %> + <% + default_mapping = { + # loops defaults + 'email' => 'email', + 'firstName' => 'first_name', + 'lastName' => 'last_name', + 'addressLine1' => 'line_1', + 'addressLine2' => 'line_2', + 'addressCity' => 'city', + 'addressState' => 'state', + 'addressZipCode' => 'postal_code', + 'addressCountry' => 'country', + # hcb promotions + 'Address (zip/postal code)' => 'postal_code', + 'Address (city)' => 'city', + 'Address (state/province)' => 'state', + 'Address (country)' => 'country', + 'Address (line 1)' => 'line_1', + 'Address (line 2)' => 'line_2', + 'Recipient Name' => 'first_name', + 'Login Email' => 'email', + } + %> + +
+ <% @csv_headers.each do |header| %> +
+
+
<%= header %>
+
+ <% if @csv_preview.first %> + <%= @csv_preview.first[@csv_headers.index(header)] %> + <% end %> +
+
+ +
+ <% + default_value = default_mapping[header] + %> + <%= select_tag "mapping[#{header}]", + options_for_select(@address_fields, default_value), + prompt: "Select address field...", + class: "form-select", + data: { csv_field: header } %> +
+
+ <% end %> +
+ + <%# Add hidden fields for required fields if they're not already mapped %> + <% + mapped_fields = @csv_headers.map { |header| default_mapping[header] }.compact + missing_required = BatchesController::REQUIRED_FIELDS - mapped_fields + + if missing_required.any? + # Find a suitable CSV header for each missing required field + missing_required.each do |field| + # Try to find a suitable header based on field name + suitable_header = case field + when 'first_name' + @csv_headers.find { |h| h.downcase.include?('name') || h.downcase.include?('recipient') } + when 'state' + @csv_headers.find { |h| h.downcase.include?('state') || h.downcase.include?('province') } + when 'line_1' + @csv_headers.find { |h| h.downcase.include?('address') || h.downcase.include?('street') } + when 'city' + @csv_headers.find { |h| h.downcase.include?('city') || h.downcase.include?('town') } + when 'postal_code' + @csv_headers.find { |h| h.downcase.include?('zip') || h.downcase.include?('postal') || h.downcase.include?('code') } + when 'country' + @csv_headers.find { |h| h.downcase.include?('country') || h.downcase.include?('nation') } + end + + if suitable_header + # Add a hidden field to map this header to the required field + hidden_field_tag "mapping[#{suitable_header}]", field + end + end + end + %> + +
+ <%= f.submit "Save Mapping", class: "btn btn-primary" %> +
+ <% end %> +
+ + + + \ No newline at end of file diff --git a/app/views/batches/new.html.erb b/app/views/batches/new.html.erb new file mode 100644 index 0000000..d1f9d4a --- /dev/null +++ b/app/views/batches/new.html.erb @@ -0,0 +1,77 @@ +<% content_for :title, "New batch" %> + +

New Batch

+ +<%= form_with(url: batches_path, scope: :batch) do |form| %> +
+ <%= form.label :type, "Batch Type" %> + <%= form.select :type, + @batch_types, + { include_blank: "select a batch type..." }, + { onchange: "toggleFields(this.value)" } %> +
+ + + + + +
+ <%= form.label :csv, "CSV File" %> + <%= form.file_field :csv, accept: ".csv" %> +
+ <%= render 'shared/tag_picker', form: form, field_name: :tags %> + + +
+ <%= form.submit "Create Batch" %> + <%= link_to "Back to batches", batches_path %> +
+<% end %> + + \ No newline at end of file diff --git a/app/views/batches/process_letter.html.erb b/app/views/batches/process_letter.html.erb new file mode 100644 index 0000000..ae92aa6 --- /dev/null +++ b/app/views/batches/process_letter.html.erb @@ -0,0 +1,292 @@ +

Process Letter Batch #<%= @batch.id %>

+ +

This will generate labels for <%= pluralize(@batch.addresses.count, 'address') %>.

+ +<%= form_with(model: @batch, url: process_batch_path(@batch), method: :post) do |form| %> +
+ <%= form.label :user_facing_title, "Letter Title", class: "form-label" %> +
This title will be visible to recipients on their letters. For example: "Monthly Newsletter" or "Important Update".
+ <%= form.text_field :user_facing_title, class: "form-control", placeholder: "e.g. Monthly Newsletter" %> +
+ +
+ <%= form.label :template_cycle, "Label Templates" %> +
Select multiple templates to cycle through them, or just one for all labels.
+ +
+ +
+ <%= form.label :letter_mailing_date, "Mailing Date" %> +
Select the date you plan to mail these letters.
+ <%= form.date_field :letter_mailing_date, + value: @batch.letter_mailing_date || @batch.default_mailing_date, + min: Date.current, + class: "form-control", + required: true, + name: "batch[letter_mailing_date]" %> +
+ +
+ <%= form.label :usps_payment_account_id, "USPS Payment Account" %> +
Select the USPS payment account to use for purchasing postage. Required only when using indicia.
+ <%= form.collection_select :usps_payment_account_id, + USPS::PaymentAccount.all, + :id, + :display_name, + { prompt: "Select a payment account" }, + { class: "form-control", + required: false, + data: { + required_when: "indicia", + us_postage_type: "batch[us_postage_type]", + intl_postage_type: "batch[intl_postage_type]" + } } %> +
+ +
+
+ <%= form.check_box :include_qr_code, id: "batch_include_qr_code", checked: true %> + <%= form.label :include_qr_code, "Include QR Code on Labels", for: "batch_include_qr_code" %> +
+
Add a QR code to each label for tracking purposes.
+
+ +
+

Postage Options

+
+
+
+
+

US Mail

+
+
+ <%= form.radio_button :us_postage_type, "stamps", class: "form-check-input", checked: true %> + <%= form.label :us_postage_type_stamps, "Stamps", class: "form-check-label" %> +
+
+ <%= form.radio_button :us_postage_type, "indicia", class: "form-check-input" %> + <%= form.label :us_postage_type_indicia, "Indicia (Metered)", class: "form-check-label" %> +
+
+
+
+

International Mail

+
+
+ <%= form.radio_button :intl_postage_type, "stamps", class: "form-check-input", checked: true %> + <%= form.label :intl_postage_type_stamps, "Stamps", class: "form-check-label" %> +
+
+ <%= form.radio_button :intl_postage_type, "indicia", class: "form-check-input" %> + <%= form.label :intl_postage_type_indicia, "Indicia (Metered)", class: "form-check-label" %> +
+
+
+
+
+
+
+ +
+

Cost Information

+
+
+
+
+
+ Total Postage Cost: + $<%= sprintf('%.2f', @batch.postage_cost) %> +
+
+ US Cost Difference: + + $<%= sprintf('%.2f', @batch.postage_cost_difference[:us]) %> + +
+
+ International Cost Difference: + + $<%= sprintf('%.2f', @batch.postage_cost_difference[:intl]) %> + +
+
+ + <% + us_count = @batch.letters.joins(:address).where(addresses: { country: "US" }).count + intl_count = @batch.letters.joins(:address).where.not(addresses: { country: "US" }).count + us_stamps = us_count # Default to stamps for US + intl_stamps = intl_count # Default to stamps for international + total_stamps = us_stamps + intl_stamps + %> + <% if total_stamps > 0 %> + You'll have to put stamps on <%= total_stamps %> envelope<%= total_stamps == 1 ? '' : 's' %> + <% end %> + <% if @batch.postage_cost_difference[:us] < 0 %> + <% if total_stamps > 0 %>; <% end %>Savings from using metered postage for US mail + <% end %> + <% if @batch.postage_cost_difference[:intl] > 0 %> + <% if total_stamps > 0 || @batch.postage_cost_difference[:us] < 0 %>; <% end %>Additional cost for international metered postage + <% end %> + <% if total_stamps == 0 && @batch.postage_cost_difference[:us] == 0 && @batch.postage_cost_difference[:intl] == 0 %> + No cost difference + <% end %> + +
+
+
+
+
+
+ +
+ <%= form.submit "Generate Labels", class: "button primary" %> + <%= link_to "Back to batch", @batch, class: "button secondary" %> +
+<% end %> + +<%= javascript_tag do %> + document.addEventListener('DOMContentLoaded', function() { + const usPostageType = document.querySelector('input[name="batch[us_postage_type]"]:checked').value; + const intlPostageType = document.querySelector('input[name="batch[intl_postage_type]"]:checked').value; + const paymentAccountSelect = document.querySelector('select[name="batch[usps_payment_account_id]"]'); + const templateSelect = document.querySelector('#batch_template_cycle'); + + // Prevent empty values in template cycle + templateSelect.addEventListener('change', function() { + const selectedOptions = Array.from(this.selectedOptions); + if (selectedOptions.length === 0) { + // If nothing is selected, select the first option + this.options[0].selected = true; + } + }); + + function updatePaymentAccountRequired() { + const usPostageType = document.querySelector('input[name="batch[us_postage_type]"]:checked').value; + const intlPostageType = document.querySelector('input[name="batch[intl_postage_type]"]:checked').value; + + const isIndiciaSelected = usPostageType === "indicia" || intlPostageType === "indicia"; + paymentAccountSelect.required = isIndiciaSelected; + } + + function updatePostageCosts() { + const usPostageType = document.querySelector('input[name="batch[us_postage_type]"]:checked').value; + const intlPostageType = document.querySelector('input[name="batch[intl_postage_type]"]:checked').value; + + fetch(`<%= update_costs_batch_path(@batch) %>`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content + }, + body: JSON.stringify({ + us_postage_type: usPostageType, + intl_postage_type: intlPostageType + }) + }) + .then(response => response.json()) + .then(data => { + document.getElementById('total_postage_cost').textContent = `$${data.total_cost.toFixed(2)}`; + document.getElementById('us_cost_difference').textContent = `$${data.cost_difference.us.toFixed(2)}`; + document.getElementById('intl_cost_difference').textContent = `$${data.cost_difference.intl.toFixed(2)}`; + + const usDiffElement = document.getElementById('us_cost_difference'); + const intlDiffElement = document.getElementById('intl_cost_difference'); + const explanationElement = document.getElementById('cost_explanation'); + + // Update US cost difference styling + if (data.cost_difference.us < 0) { + usDiffElement.classList.remove('text-red-600'); + usDiffElement.classList.add('text-green-600'); + } else { + usDiffElement.classList.remove('text-green-600'); + usDiffElement.classList.add('text-red-600'); + } + + // Update international cost difference styling + if (data.cost_difference.intl < 0) { + intlDiffElement.classList.remove('text-red-600'); + intlDiffElement.classList.add('text-green-600'); + } else { + intlDiffElement.classList.remove('text-green-600'); + intlDiffElement.classList.add('text-red-600'); + } + + // Update explanation text + let explanation = []; + + // Add stamp information if either option is stamps + const usPostageType = document.querySelector('input[name="batch[us_postage_type]"]:checked').value; + const intlPostageType = document.querySelector('input[name="batch[intl_postage_type]"]:checked').value; + + if (usPostageType === "stamps" || intlPostageType === "stamps") { + const usCount = usPostageType === "stamps" ? data.us_count || 0 : 0; + const intlCount = intlPostageType === "stamps" ? data.intl_count || 0 : 0; + const totalStamps = usCount + intlCount; + + if (totalStamps > 0) { + explanation.push(`You'll have to put stamps on ${totalStamps} envelope${totalStamps === 1 ? '' : 's'}`); + } + } + + // Add cost difference information + if (data.cost_difference.us < 0) { + explanation.push('Savings from using metered postage for US mail'); + } + if (data.cost_difference.intl > 0) { + explanation.push('Additional cost for international metered postage'); + } + if (explanation.length === 0) { + explanation.push('No cost difference'); + } + + explanationElement.textContent = explanation.join('; '); + }); + } + + document.querySelectorAll('input[name="batch[us_postage_type]"]').forEach(input => { + input.addEventListener('change', () => { + updatePaymentAccountRequired(); + updatePostageCosts(); + }); + }); + + document.querySelectorAll('input[name="batch[intl_postage_type]"]').forEach(input => { + input.addEventListener('change', () => { + updatePaymentAccountRequired(); + updatePostageCosts(); + }); + }); + + // Initial check + updatePaymentAccountRequired(); + }); +<% end %> + + + <%# @batch.warehouse_template.line_items.each do |line_item| %> + + <%# end %> + + + + + + \ No newline at end of file diff --git a/app/views/batches/process_warehouse.html.erb b/app/views/batches/process_warehouse.html.erb new file mode 100644 index 0000000..9ccfd3c --- /dev/null +++ b/app/views/batches/process_warehouse.html.erb @@ -0,0 +1,14 @@ +

Process batch #<%= @batch.id %>

+This will create <%= @batch.addresses.count %> warehouse orders, containing:
+ + +The contents will cost <%= number_to_currency @batch.contents_cost %>
+The warehouse labor will cost <%= number_to_currency @batch.labor_cost %>
+The postage will cost some indeterminate amount.
+Total: ~<%= number_to_currency @batch.total_cost %> + +<%= button_to "do it!", process_batch_path(@batch), method: :post %> \ No newline at end of file diff --git a/app/views/batches/show.html.erb b/app/views/batches/show.html.erb new file mode 100644 index 0000000..92a944b --- /dev/null +++ b/app/views/batches/show.html.erb @@ -0,0 +1,71 @@ +<% content_for :title, "#{@batch.type.split('::').first.titleize} Batch ##{@batch.id} - #{@batch.addresses.count} #{@batch.type.split('::').first.downcase.pluralize}" %> + +
+ + + <%= render @batch %> + + <% if @batch.is_a?(Letter::Batch) && @batch.processed? %> + <%= render partial: "letter_batch", locals: { batch: @batch } %> + + <% if @batch.letters.any? %> +
+
+ +

Letters (<%= @batch.letters.count %>)

+
+
+
+ <%= render partial: 'letters_collection', locals: { letters: @batch.letters } %> +
+
+
+ <% end %> + <% end %> + + <% if @batch.is_a?(Warehouse::Batch) && @batch.orders.any? %> +
+
+ +

Orders (<%= @batch.orders.count %>)

+
+
+
+ <%= render partial: 'orders_collection', locals: { orders: @batch.orders } %> +
+
+
+ <% end %> + + <%= render partial: 'admin_inspector', locals: { record: @batch } %> + + <% if @batch.addresses.any? %> +
+
+ +

Addresses (<%= @batch.addresses.count %>)

+
+
+
+ <%= render partial: 'addresses_table', locals: { addresses: @batch.addresses } %> +
+
+
+ <% end %> + +
+

Danger Zone

+ <%= danger_button_to "Delete this batch", @batch, method: :delete, data: { confirm: "Are you sure you want to delete this batch? This action cannot be undone." } %> +
+
diff --git a/app/views/batches/show.json.jbuilder b/app/views/batches/show.json.jbuilder new file mode 100644 index 0000000..1e697b5 --- /dev/null +++ b/app/views/batches/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "batches/batch", batch: @batch diff --git a/app/views/customs_receipt/receipt_mailer/receipt.text.erb b/app/views/customs_receipt/receipt_mailer/receipt.text.erb new file mode 100644 index 0000000..afee7c5 --- /dev/null +++ b/app/views/customs_receipt/receipt_mailer/receipt.text.erb @@ -0,0 +1,14 @@ +<%= { + transactionalId: "cmb15i7wg0u300q0ihjgd1n7e", + email: @recipient, + dataVariables: { + order_number: @order_number + }, + attachments: [ + { + filename: "customs-receipt.pdf", + contentType: "application/pdf", + data: Base64.strict_encode64(@pdf_data) + } + ] + }.to_json %> \ No newline at end of file diff --git a/app/views/customs_receipts/index.html.erb b/app/views/customs_receipts/index.html.erb new file mode 100644 index 0000000..b29ca4b --- /dev/null +++ b/app/views/customs_receipts/index.html.erb @@ -0,0 +1,6 @@ +

generate customs receipt

+<%= form_with url: generate_customs_receipts_path, method: :get do |f| %> + <%= f.label :search, "search:" %> + <%= f.text_field :search, placeholder: "ID (recASDF/pkg!SDfJK) or original tracking number (ASnnnnn...)" %> + <%= f.submit "Generate" %> +<% end %> \ No newline at end of file diff --git a/app/views/customs_receipts/show.html.erb b/app/views/customs_receipts/show.html.erb new file mode 100644 index 0000000..e69de29 diff --git a/app/views/fields/array/_form.html.erb b/app/views/fields/array/_form.html.erb new file mode 100644 index 0000000..5be331a --- /dev/null +++ b/app/views/fields/array/_form.html.erb @@ -0,0 +1,7 @@ +
+ <%= f.label field.attribute %> +
+
+ <%= f.text_field field.attribute, value: field.data&.join(", "), name: "#{f.object_name}[#{field.attribute}][]" %> +

Enter values separated by commas

+
\ No newline at end of file diff --git a/app/views/fields/array/_index.html.erb b/app/views/fields/array/_index.html.erb new file mode 100644 index 0000000..5bcb13b --- /dev/null +++ b/app/views/fields/array/_index.html.erb @@ -0,0 +1,3 @@ +<% if field.data.present? %> + <%= field.data.join(", ") %> +<% end %> \ No newline at end of file diff --git a/app/views/fields/array/_show.html.erb b/app/views/fields/array/_show.html.erb new file mode 100644 index 0000000..b812550 --- /dev/null +++ b/app/views/fields/array/_show.html.erb @@ -0,0 +1,7 @@ +<% if field.data.present? %> +
+ <% field.data.each do |item| %> + <%= item %> + <% end %> +
+<% end %> \ No newline at end of file diff --git a/app/views/good_job/shared/_secondary_navbar.erb b/app/views/good_job/shared/_secondary_navbar.erb new file mode 100644 index 0000000..74728b5 --- /dev/null +++ b/app/views/good_job/shared/_secondary_navbar.erb @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/app/views/inspect/show.html.erb b/app/views/inspect/show.html.erb new file mode 100644 index 0000000..104d504 --- /dev/null +++ b/app/views/inspect/show.html.erb @@ -0,0 +1,14 @@ +

inspektor

+<%= render "admin_inspector", record: @record %> +associated fields:
+ diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb new file mode 100644 index 0000000..3ab5890 --- /dev/null +++ b/app/views/layouts/application.html.erb @@ -0,0 +1,43 @@ + + + + <%= content_for(:title) || "Theseus" %> + + + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= yield :head %> + + <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %> + <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %> + + + + + + <%= vite_client_tag %> + <%= vite_javascript_tag 'application' %> + <%= vite_stylesheet_tag 'app_style.scss' %> + + + + + class="not_prod"<% end %>> + <% if Rails.env.development? %> +
+ <% end %> + +
+ <%= render "shared/nav" %> + +
+
+ <%= render "shared/flash" %> + <%= yield %> +
+
+
+ + diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000..37f0bdd --- /dev/null +++ b/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/app/views/layouts/public.html.erb b/app/views/layouts/public.html.erb new file mode 100644 index 0000000..94daa29 --- /dev/null +++ b/app/views/layouts/public.html.erb @@ -0,0 +1,41 @@ + + + + <%= content_for(:title) || "Hack Club Mail" %> + + + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= yield :head %> + + <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %> + <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %> + + + + + + <%= vite_client_tag %> + <%= vite_javascript_tag 'public' %> + <%= vite_stylesheet_tag 'public.scss' %> + <%= vite_stylesheet_tag '98.css' %> + + + + + class="not_prod" +<% end %> data-turbo="false"> +<% if Rails.env.development? %> +
+<% end %> + +<%= render 'public/backend_controls' %> + +<%= render "shared/flash" %> +<%= yield %> + + + diff --git a/app/views/layouts/public/frameable.html.erb b/app/views/layouts/public/frameable.html.erb new file mode 100644 index 0000000..74164c7 --- /dev/null +++ b/app/views/layouts/public/frameable.html.erb @@ -0,0 +1,38 @@ + +class="hframed" + <% end %>> + + <%= content_for(:title) || "Hack Club Mail" %> + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + <%= vite_client_tag %> + <%= vite_javascript_tag 'public' %> + <%= vite_stylesheet_tag 'public.scss' %> + <%= vite_stylesheet_tag '98.css' %> + <%= yield :head %> + +class="framed" + <% end %>> +<% unless @framed %> + <%= render 'public/backend_controls' %> +
+
+
<%= yield :window_title %>
+
+ +
+
+
+ <% end %> + <%= render 'shared/flash' %> + <%= yield %> + <% unless @framed %> +
+
+<% end %> + + \ No newline at end of file diff --git a/app/views/layouts/public/mailer.html.erb b/app/views/layouts/public/mailer.html.erb new file mode 100644 index 0000000..3aac900 --- /dev/null +++ b/app/views/layouts/public/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/app/views/layouts/public/mailer.text.erb b/app/views/layouts/public/mailer.text.erb new file mode 100644 index 0000000..37f0bdd --- /dev/null +++ b/app/views/layouts/public/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/app/views/layouts/text_mailer.text.erb b/app/views/layouts/text_mailer.text.erb new file mode 100644 index 0000000..21526cd --- /dev/null +++ b/app/views/layouts/text_mailer.text.erb @@ -0,0 +1,8 @@ +<%= { + transactionalId: "cm97n3xyr01pwo99pr66jkx4v", + email: @recipient, + dataVariables: { + subject: @subject, + body: yield.html_safe + } + }.to_json %> \ No newline at end of file diff --git a/app/views/layouts/warehouse/mailer.text.erb b/app/views/layouts/warehouse/mailer.text.erb new file mode 100644 index 0000000..37f0bdd --- /dev/null +++ b/app/views/layouts/warehouse/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/app/views/letter/batches/_batch.html.erb b/app/views/letter/batches/_batch.html.erb new file mode 100644 index 0000000..93254b9 --- /dev/null +++ b/app/views/letter/batches/_batch.html.erb @@ -0,0 +1,74 @@ +
+
+
+
+

Batch Details

+
+ Created: + <%= @batch.created_at.strftime("%B %d, %Y") %> +
+
+ Status: + <%= batch_status_badge(@batch.aasm.current_state) %> +
+
+ Letters: + <%= @batch.addresses.count %> +
+ <% if @batch.user_facing_title.present? %> +
+ Title: + <%= @batch.user_facing_title %> +
+ <% end %> +
+ +
+

Letter Specifications

+
+ Weight: + <%= @batch.letter_weight %> oz +
+
+ Mailing Date: + <%= @batch.letter_mailing_date&.strftime("%B %d, %Y") %> +
+
+ Processing Category: + <%= @batch.letter_processing_category %> +
+
+ Mailer ID: + <%= @batch.mailer_id&.display_name %> +
+
+ Return Address: + <%= @batch.letter_return_address&.display_name %> +
+
+
+ + <%= render 'shared/tags', tags: @batch.tags %> +
+
+ +
+
+

Actions

+
+
+ <% case @batch.aasm_state %> + <% when "awaiting_field_mapping" %> + <%= warning_link_to "Map Fields", map_fields_letter_batch_path(@batch), class: "w-full mb-2" do %> + + + + Map Fields + <% end %> + <% when "fields_mapped" %> + <%= success_link_to "Process Batch", process_confirm_letter_batch_path(@batch), class: "w-full" do %> + <%= check_icon %>Process Batch + <% end %> + <% end %> +
+
\ No newline at end of file diff --git a/app/views/letter/batches/_batches_collection.html.erb b/app/views/letter/batches/_batches_collection.html.erb new file mode 100644 index 0000000..2ec5733 --- /dev/null +++ b/app/views/letter/batches/_batches_collection.html.erb @@ -0,0 +1,35 @@ +<% batches.each do |batch| %> +
+
+
+

+ <%= link_to "Batch ##{batch.id}", letter_batch_path(batch) %> + (<%= pluralize(batch.letters.count, 'letter') %>) + <%= batch_status_badge(batch.aasm.current_state, 'float-right') %> +

+
+
+
+ Created: + <%= time_ago_in_words(batch.created_at) %> ago +
+
Tags: <%= render 'shared/tags', tags: batch.tags %>
+
+
+ <%= link_to letter_batch_path(batch), class: "btn btn-sm btn-icon", title: "View batch details" do %> + + + + + View + <% end %> + <%= link_to edit_letter_batch_path(batch), class: "btn btn-sm btn-icon", title: "Edit batch" do %> + + + + Edit + <% end %> +
+
+
+<% end %> \ No newline at end of file diff --git a/app/views/letter/batches/_form.html.erb b/app/views/letter/batches/_form.html.erb new file mode 100644 index 0000000..a209b0f --- /dev/null +++ b/app/views/letter/batches/_form.html.erb @@ -0,0 +1,51 @@ +<%= form_with(model: batch, url: letter_batches_path, scope: :letter_batch) do |form| %> + <% if batch.errors.any? %> +
+

<%= pluralize(batch.errors.count, "error") %> prohibited this batch from being saved:

+ +
    + <% batch.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> + + <%= render partial: "letters/letter_attributes_form", locals: { form: form, is_batch: true } %> + +
+ <%= form.label :letter_mailer_id_id, "USPS Mailer ID" %> + <%= form.collection_select :letter_mailer_id_id, + USPS::MailerId.all, + :id, + :display_name, + { selected: current_user.home_mid_id } %> +
+ +
+ <%= form.label :letter_return_address_id, "Return Address" %> + <%= link_to "(manage)", return_addresses_path %> + <%= form.collection_select :letter_return_address_id, + ReturnAddress.shared.or(ReturnAddress.owned_by(current_user)), + :id, + :display_name, + { selected: current_user.home_return_address_id } %> +
+ +
+ <%= form.label :letter_return_address_name, "Custom Return Address Name (optional)", class: "form-label" %> + <%= form.text_field :letter_return_address_name, class: "form-control", placeholder: "Leave blank to use the return address name" %> +
+ +
+ <%= form.label :csv, "CSV File" %> + <%= form.file_field :csv, accept: ".csv" %> +
+ + <%= render 'shared/tag_picker', form: form, field_name: :tags %> + +
+ <%= form.submit "Create Batch" %> + <%= link_to "Back to batches", letter_batches_path %> +
+<% end %> \ No newline at end of file diff --git a/app/views/letter/batches/edit.html.erb b/app/views/letter/batches/edit.html.erb new file mode 100644 index 0000000..7235c7b --- /dev/null +++ b/app/views/letter/batches/edit.html.erb @@ -0,0 +1,81 @@ +<% content_for :title, "Edit Letter Batch ##{@batch.id}" %> + +
+ + + <%= form_with(model: @batch, url: letter_batch_path(@batch), method: :patch, scope: :letter_batch) do |form| %> + <% if @batch.errors.any? %> +
+

<%= pluralize(@batch.errors.count, "error") %> prohibited this batch from being saved:

+
    + <% @batch.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> + +
+
+

Always Editable

+
+
+
+ <%= form.label :user_facing_title, "Title", class: "form-label" %> + <%= form.text_field :user_facing_title, class: "form-control" %> +
+ + <%= render 'shared/tag_picker', form: form, field_name: :tags %> +
+
+ + <% if @batch.may_mark_processed? %> +
+
+

Editable Before Processing

+
+
+ <%= render partial: "letters/letter_attributes_form", locals: { form: form, is_batch: true } %> + +
+ <%= form.label :letter_mailer_id_id, "USPS Mailer ID", class: "form-label" %> + <%= form.collection_select :letter_mailer_id_id, + USPS::MailerId.all, + :id, + :display_name, + { selected: @batch.letter_mailer_id_id }, + class: "form-select" %> +
+ +
+ <%= form.label :letter_return_address_id, "Return Address", class: "form-label" %> + <%= link_to "(manage)", return_addresses_path, class: "text-sm" %> + <%= form.collection_select :letter_return_address_id, + ReturnAddress.shared.or(ReturnAddress.owned_by(current_user)), + :id, + :display_name, + { selected: @batch.letter_return_address_id }, + class: "form-select" %> +
+ +
+ <%= form.label :letter_return_address_name, "Custom Return Address Name (optional)", class: "form-label" %> + <%= form.text_field :letter_return_address_name, class: "form-control", placeholder: "Leave blank to use the return address name" %> +
+
+
+ <% end %> + +
+ <%= form.submit "Update Batch", class: "btn btn-primary" %> + <%= link_to "Cancel", letter_batch_path(@batch), class: "btn btn-secondary" %> +
+ <% end %> +
\ No newline at end of file diff --git a/app/views/letter/batches/index.html.erb b/app/views/letter/batches/index.html.erb new file mode 100644 index 0000000..c5d8881 --- /dev/null +++ b/app/views/letter/batches/index.html.erb @@ -0,0 +1,18 @@ +<% content_for :title, "Letter Batches" %> +
+ +
+
+ <% if @batches.any? %> + <%= render partial: 'letter/batches/batches_collection', locals: { batches: @batches } %> + <% else %> +

No letter batches found.

+ <% end %> +
+
+
\ No newline at end of file diff --git a/app/views/letter/batches/map_fields.html.erb b/app/views/letter/batches/map_fields.html.erb new file mode 100644 index 0000000..239038d --- /dev/null +++ b/app/views/letter/batches/map_fields.html.erb @@ -0,0 +1,2 @@ +<% content_for :title, "Map Fields - Letter Batch ##{@batch.id}" %> +<%= render 'shared/map_fields' %> \ No newline at end of file diff --git a/app/views/letter/batches/new.html.erb b/app/views/letter/batches/new.html.erb new file mode 100644 index 0000000..96408f3 --- /dev/null +++ b/app/views/letter/batches/new.html.erb @@ -0,0 +1,14 @@ +<% content_for :title, "New Letter Batch" %> + +
+ + + <%= render "form", batch: @batch %> +
\ No newline at end of file diff --git a/app/views/letter/batches/process.html.erb b/app/views/letter/batches/process.html.erb new file mode 100644 index 0000000..b08a26e --- /dev/null +++ b/app/views/letter/batches/process.html.erb @@ -0,0 +1,18 @@ +
+ <%= form.label :letter_mailing_date, "Mailing Date", class: "form-label" %> + <%= form.date_field :letter_mailing_date, + value: @batch.letter_mailing_date || @batch.default_mailing_date, + min: Date.current, + class: "form-control", + required: true %> +
+ +
+ <%= form.label :usps_payment_account_id, "Payment Account", class: "form-label" %> + <%= form.collection_select :usps_payment_account_id, + USPS::PaymentAccount.all, + :id, + :name, + { prompt: "Select a payment account" }, + { class: "form-control", required: true } %> +
\ No newline at end of file diff --git a/app/views/letter/batches/process_letter.html.erb b/app/views/letter/batches/process_letter.html.erb new file mode 100644 index 0000000..30da054 --- /dev/null +++ b/app/views/letter/batches/process_letter.html.erb @@ -0,0 +1,263 @@ +<%# Process Letter Batch Form %> +
+
+
+

Process Letter Batch

+

Review the batch details and configure processing options.

+
+
+
+ <% if @batch.letter_return_address&.us? %> +
+
+

Letter Counts

+
+ US Letters: + + <%= @batch.letters.joins(:address).where(addresses: { country: "US" }).count %> + +
+
+ International Letters: + + <%= @batch.letters.joins(:address).where.not(addresses: { country: "US" }).count %> + +
+
+ Total Letters: + + <%= @batch.letters.count %> + +
+
+
+

Projected Costs

+
+ Total Postage Cost: + $<%= sprintf('%.2f', @batch.postage_cost) %> +
+
+ US Cost Difference: + + $<%= sprintf('%.2f', @batch.postage_cost_difference[:us]) %> + +
+
+ International Cost Difference: + + $<%= sprintf('%.2f', @batch.postage_cost_difference[:intl]) %> + +
+
+
+ <% else %> +
+ Total Letters: + + <%= @batch.letters.count %> + +
+ <% end %> +
+
+ <%= form_with(model: @batch, url: process_letter_batch_path(@batch), method: :post) do |f| %> +
+
+ <%= f.label :letter_mailing_date, "Mailing Date", class: "form-label" %> + <%= f.date_field :letter_mailing_date, + value: @batch.letter_mailing_date || @batch.default_mailing_date, + min: Date.current, + class: "form-control", + required: true %> +
+ <% if @batch.letter_return_address&.us? %> +
+

US Mail

+
+
+ <%= f.radio_button :us_postage_type, "stamps", class: "form-check-input", checked: true %> + <%= f.label :us_postage_type_stamps, "Stamps", class: "form-check-label" %> +
+
+ <%= f.radio_button :us_postage_type, "indicia", class: "form-check-input" %> + <%= f.label :us_postage_type_indicia, "Indicia (Metered)", class: "form-check-label" %> +
+
+
+
+

International Mail

+
+
+ <%= f.radio_button :intl_postage_type, "stamps", class: "form-check-input", checked: true %> + <%= f.label :intl_postage_type_stamps, "Stamps", class: "form-check-label" %> +
+
+ <%= f.radio_button :intl_postage_type, "indicia", class: "form-check-input" %> + <%= f.label :intl_postage_type_indicia, "Indicia (Metered)", class: "form-check-label" %> +
+
+
+ <% else %> + + <%= f.hidden_field :us_postage_type, value: "international_origin" %> + <%= f.hidden_field :intl_postage_type, value: "international_origin" %> + <% end %> + +
+ <%= f.label :template_cycle, "Template Cycle", class: "form-label" %> + <%= f.hidden_field :template_cycle, id: "selected_template", value: params[:template] %> + <%= render "shared/template_picker", multiple: true %> +
+
+ <%= f.label :user_facing_title, "User Facing Title", class: "form-label" %> + <%= f.text_field :user_facing_title, class: "form-control", value: params[:uft] %> +
+
+ <%= f.label :include_qr_code, "Include QR Code", class: "form-label" %> + <%= f.check_box :include_qr_code, class: "form-checkbox", checked: true %> +
+
+ <%= f.label :immediate_print, "⚡ print immediately when done?", class: "form-label" %> + <%= f.check_box :immediate_print, class: "form-checkbox", checked: false %> +
+
+
+ <%= f.submit "Process Batch", class: "contrast" %> + <%= link_to "Cancel", letter_batch_path(@batch), class: "secondary" %> +
+ <% end %> +
+
+ + \ No newline at end of file diff --git a/app/views/letter/batches/regenerate_labels.erb b/app/views/letter/batches/regenerate_labels.erb new file mode 100644 index 0000000..01d9aa7 --- /dev/null +++ b/app/views/letter/batches/regenerate_labels.erb @@ -0,0 +1,16 @@ +

regenerate labels

+<%= form_with(model: @batch, url: regenerate_labels_letter_batch_path(@batch), method: :post) do |f| %> +
+ <%= f.label :template_cycle, "templates to use:", class: "form-label" %> + <%= f.hidden_field :template_cycle, id: "selected_template", value: params[:template] %> + <%= render "shared/template_picker", multiple: true %> +
+
+ <%= f.label :include_qr_code, "Include QR Code", class: "form-label" %> + <%= f.check_box :include_qr_code, class: "form-checkbox", checked: true %> +
+
+ <%= f.submit "Regenerate Labels", class: "contrast" %> + <%= link_to "nevermind", letter_batch_path(@batch)%> +
+<% end %> diff --git a/app/views/letter/batches/show.html.erb b/app/views/letter/batches/show.html.erb new file mode 100644 index 0000000..25e4ac9 --- /dev/null +++ b/app/views/letter/batches/show.html.erb @@ -0,0 +1,152 @@ +<% content_for :title, "Letter Batch ##{@batch.id} - #{@batch.addresses.count} letters" %> +
+ +
\ No newline at end of file diff --git a/app/views/letter/instant_queues/_instant_queue.html.erb b/app/views/letter/instant_queues/_instant_queue.html.erb new file mode 100644 index 0000000..2926bb6 --- /dev/null +++ b/app/views/letter/instant_queues/_instant_queue.html.erb @@ -0,0 +1,34 @@ +<%= render "letter/queues/queue", queue: instant_queue %> +
+

Instant Queue Details

+ <% if instant_queue.user_facing_title.present? %> +

+ Display Title: + <%= instant_queue.user_facing_title %> +

+ <% end %> +

+ Template: + <%= instant_queue.template %> +

+

+ Postage Type: + <%= instant_queue.postage_type.titleize %> +

+ <% if instant_queue.indicia? %> +

+ Payment Account: + <%= instant_queue.usps_payment_account&.display_name %> +

+ <% end %> +

+ QR Code: + <%= instant_queue.include_qr_code ? "Yes" : "No" %> +

+ <% if instant_queue.letter_mailing_date %> +

+ Mailing Date: + <%= instant_queue.letter_mailing_date.strftime("%B %d, %Y") %> +

+ <% end %> +
\ No newline at end of file diff --git a/app/views/letter/queues/_form.html.erb b/app/views/letter/queues/_form.html.erb new file mode 100644 index 0000000..533eebd --- /dev/null +++ b/app/views/letter/queues/_form.html.erb @@ -0,0 +1,125 @@ +<%= form_with(model: letter_queue, + url: letter_queue.is_a?(Letter::InstantQueue) ? + (letter_queue.new_record? ? letter_instant_queues_path : letter_instant_queue_path(letter_queue)) : + (letter_queue.new_record? ? letter_queues_path : letter_queue_path(letter_queue)), + scope: letter_queue.is_a?(Letter::InstantQueue) ? :letter_instant_queue : :letter_queue) do |form| %> + <% if letter_queue.errors.any? %> +
+

<%= pluralize(letter_queue.errors.count, "error") %> prohibited this letter_queue from being saved:

+
    + <% letter_queue.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> +

the important part:

+
+
+ <%= form.label :name, class: "form-label" %> + <%= form.text_field :name, class: "form-control" %> +
+
+ <%= form.label :user_facing_title, "Display Title", class: "form-label" %> + <%= form.text_field :user_facing_title, class: "form-control", placeholder: "Optional title that will be shown to users" %> +
+
+ <%= render 'shared/tag_picker', form: form %> +
+
+ <% if letter_queue.new_record? %> +
+ <%= form.label :type, "Queue Type", class: "form-label" %> + <%= form.select :type, + [ + ["Regular Queue", "Letter::Queue"], + ["Instant Queue", "Letter::InstantQueue"] + ], + { selected: letter_queue.type || "Letter::Queue" }, + { class: "form-control", onchange: "toggleQueueType(this)" } %> +
+ <% end %> +

set defaults for letters:

+
+ <%= form.label :template, "Label Template", class: "form-label" %> + <%= form.hidden_field :template, id: "selected_template", value: letter_queue.template %> + <%= render "shared/template_picker", multiple: false, show_all: true %> +
+ <%= render 'letters/letter_attributes_form', form: form, is_batch: true %> +
+ <%= form.label :letter_mailer_id_id, "USPS Mailer ID", class: "form-label" %> + <%= form.collection_select :letter_mailer_id_id, + USPS::MailerId.all, + :id, + :display_name, + { selected: current_user.home_mid_id }, + { class: "form-control" } %> +
+
+ <%= form.label :letter_return_address_id, "Return Address", class: "form-label" %> + <%= link_to "(manage)", return_addresses_path, class: "text-sm" %> + <%= form.collection_select :letter_return_address_id, + ReturnAddress.shared.or(ReturnAddress.owned_by(current_user)), + :id, + :display_name, + { selected: current_user.home_return_address_id }, + { class: "form-control" } %> +
+
+ <%= form.label :letter_return_address_name, "custom return address name", class: "form-label" %> + <%= form.text_field :letter_return_address_name, class: "form-control", placeholder: "leave blank to use the address' default name" %> +
+ <%# Instant Queue specific fields %> +
+

Instant Queue Settings

+
+ <%= form.label :postage_type, "Postage Type", class: "form-label" %> + <%= form.select :postage_type, + [["Indicia", "indicia"], ["Stamps", "stamps"], ["International Origin", "international_origin"]], + { selected: letter_queue.postage_type || "indicia" }, + { class: "form-control", onchange: "togglePaymentAccount(this)" } %> +
+
+ <%= form.label :usps_payment_account_id, "USPS Payment Account", class: "form-label" %> + <%= form.collection_select :usps_payment_account_id, + USPS::PaymentAccount.all, + :id, + :display_name, + { selected: letter_queue.usps_payment_account_id }, + { class: "form-control" } %> +
+
+ <%= form.label :include_qr_code, "Include QR Code", class: "form-label" %> + <%= form.check_box :include_qr_code, class: "form-checkbox", checked: letter_queue.include_qr_code.nil? ? true : letter_queue.include_qr_code %> +
+
+ <%= form.label :letter_mailing_date, "Mailing Date", class: "form-label" %> + <%= form.date_field :letter_mailing_date, + class: "form-control", + value: letter_queue.letter_mailing_date || Date.current %> +
+
+ <% admin_tool do %> +
+ <%= form.label :slug, style: "display: block" %> + <%= form.text_field :slug %> +
+ <% end %> +
+ <%= form.submit class: "btn success" %> +
+<% end %> + diff --git a/app/views/letter/queues/_letter_queue.json.jbuilder b/app/views/letter/queues/_letter_queue.json.jbuilder new file mode 100644 index 0000000..83d5d07 --- /dev/null +++ b/app/views/letter/queues/_letter_queue.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! letter_queue, :id, :name, :slug, :user_id, :created_at, :updated_at +json.url letter_queue_url(letter_queue, format: :json) diff --git a/app/views/letter/queues/_queue.html.erb b/app/views/letter/queues/_queue.html.erb new file mode 100644 index 0000000..98243db --- /dev/null +++ b/app/views/letter/queues/_queue.html.erb @@ -0,0 +1,40 @@ +
+

+ Name: + <%= queue.name %> +

+ <% if queue.user_facing_title.present? %> +

+ Display Title: + <%= queue.user_facing_title %> +

+ <% end %> +

+ Type: + <%= queue.type&.constantize&.model_name&.human || "Regular Queue" %> +

+

+ Slug: + <%= queue.slug %> +

+

+ Owner: + <%= render "shared/user_mention", user: queue.user %> +

+ <% if queue.is_a?(Letter::InstantQueue) %> +

+ Template: + <%= queue.template %> +

+

+ Postage Type: + <%= queue.postage_type.titleize %> +

+ <% if queue.indicia? %> +

+ Payment Account: + <%= queue.usps_payment_account&.display_name %> +

+ <% end %> + <% end %> +
diff --git a/app/views/letter/queues/edit.html.erb b/app/views/letter/queues/edit.html.erb new file mode 100644 index 0000000..ff3c63a --- /dev/null +++ b/app/views/letter/queues/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing queue" %> + +

Editing queue

+ +<%= render "form", letter_queue: @letter_queue %> + +
+ +
+ <%= link_to "Show this queue", @letter_queue %> | + <%= link_to "Back to queues", letter_queues_path %> +
diff --git a/app/views/letter/queues/index.html.erb b/app/views/letter/queues/index.html.erb new file mode 100644 index 0000000..b599b60 --- /dev/null +++ b/app/views/letter/queues/index.html.erb @@ -0,0 +1,28 @@ +

<%= notice %>

+<% content_for :title, "Queues" %> +

Queues

+<% admin_tool do %> +
+ <%= button_to mark_printed_instants_mailed_letter_queues_path, + method: :post, + class: "btn success" do %> + + + + i just dumped the printer output tray into the mailbox + <% end %> +
+<% end %> +
+ <% @letter_queues.each do |letter_queue| %> +
+ <%= render letter_queue %> +

+ <%= link_to "Show this queue", + letter_queue.is_a?(Letter::InstantQueue) ? letter_instant_queue_path(letter_queue) : letter_queue_path(letter_queue) %> +

+
+ <% end %> +
+<%= link_to "New queue", new_letter_queue_path %> +<%= link_to "New instant queue", new_letter_instant_queue_path %> diff --git a/app/views/letter/queues/index.json.jbuilder b/app/views/letter/queues/index.json.jbuilder new file mode 100644 index 0000000..7187c2f --- /dev/null +++ b/app/views/letter/queues/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @letter_queues, partial: "letter/queues/letter_queue", as: :letter_queue diff --git a/app/views/letter/queues/new.html.erb b/app/views/letter/queues/new.html.erb new file mode 100644 index 0000000..15605f4 --- /dev/null +++ b/app/views/letter/queues/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "New queue" %> + +

New queue

+ +<%= render "form", letter_queue: @letter_queue %> + +
+ +
+ <%= link_to "Back to queues", letter_queues_path %> +
diff --git a/app/views/letter/queues/show.html.erb b/app/views/letter/queues/show.html.erb new file mode 100644 index 0000000..71ffe3c --- /dev/null +++ b/app/views/letter/queues/show.html.erb @@ -0,0 +1,101 @@ +

<%= notice %>

+
+ +
+
+

queue details

+
+
+

owner

+ <%= render "shared/user_mention", user: @letter_queue.user %> +
+
+

tags

+ <%= render 'shared/tags', tags: @letter_queue.tags %> +
+
+
+

return address

+ <% if @letter_queue.letter_return_address.present? %> +
+
<%= @letter_queue.letter_return_address_name || @letter_queue.letter_return_address.name %>
+
<%= @letter_queue.letter_return_address.line_1 %>
+ <% if @letter_queue.letter_return_address.line_2.present? %> +
<%= @letter_queue.letter_return_address.line_2 %>
+ <% end %> +
<%= @letter_queue.letter_return_address.city %>, <%= @letter_queue.letter_return_address.state %> <%= @letter_queue.letter_return_address.postal_code %>
+
<%= @letter_queue.letter_return_address.country %>
+
+ <% else %> +

no return address

+ <% end %> +
+
+

mailer id

+
+ <%= @letter_queue.letter_mailer_id&.display_name || "no mailer id" %> +
+
+
+

letter specs

+ <%= render 'shared/letter_attributes', record: @letter_queue %> +
+
+
+
+
+
+ api docs: +
+

what do?

+

send a POST request to <%= copy_to_clipboard @letter_queue.is_a?(Letter::InstantQueue) ? create_instant_letter_api_v1_letter_queues_url(@letter_queue) : api_v1_letter_queue_url(@letter_queue) do %> + <%= @letter_queue.is_a?(Letter::InstantQueue) ? create_instant_letter_api_v1_letter_queues_path(@letter_queue) : api_v1_letter_queue_path(@letter_queue) %> + <% end %> with JSON like:

+ <%= render_json_example({ + recipient_email: current_user.email, + address: { + first_name: "Bort", + last_name: "Fargler", + line_1: "8605 Santa Clausica Blvd.", + line_2: "PMB 86294 (optional)", + city: "West Sillywood", + state: "CA", + postal_code: '90069', + country: "United States" + }, + rubber_stamps: "extra text if the template you're using supports it", + idempotency_key: "optional but it'd be a great idea... rec#{SecureRandom.alphanumeric 12}?", + metadata: { + whatever_you_want: "this gets JSONB'd, you can stash program-specific meta info here", + seriously_any_keys_and_values: "as long as postgres can serialize it :3" + } + }) %> +

use an Authorization header of Bearer (one of your <%= link_to "API keys", api_keys_path, target: "_blank"%>)

+
+
+
+ +

letters:

+<%= render partial: 'batches/letters_collection', locals: { letters: @letters } %> +<% if @letter_queue.letters.queued.any? %> +
+ <%= button_to "make batch!", make_batch_from_letter_queue_path(@letter_queue), class: "btn success" %> +
+<% end %> +
+<% if @batches&.any? %> +

offspring:

+ <%= render partial: 'letter/batches/batches_collection', locals: { batches: @batches } %> +<% else %> +

"No batches?"

+<% end %> +<%= render "admin_inspector", record: @letter_queue %> \ No newline at end of file diff --git a/app/views/letter/queues/show.json.jbuilder b/app/views/letter/queues/show.json.jbuilder new file mode 100644 index 0000000..58851fd --- /dev/null +++ b/app/views/letter/queues/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "letter/queues/letter_queue", letter_queue: @letter_queue diff --git a/app/views/letters/_form.html.erb b/app/views/letters/_form.html.erb new file mode 100644 index 0000000..2ad8752 --- /dev/null +++ b/app/views/letters/_form.html.erb @@ -0,0 +1,146 @@ +<%= form_with(model: letter, class: "form-container") do |form| %> + <% if letter.errors.any? %> +
+

<%= pluralize(letter.errors.count, "error") %> prohibited this letter from being saved:

+
    + <% letter.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> +
+ <%= form.label :user_facing_title, "Title", class: "form-label" %> + <%= form.text_field :user_facing_title, class: "form-control", placeholder: "e.g. Monthly Newsletter" %> +
+
+ Letter Specs + <%= render partial: "letters/letter_attributes_form", locals: { form: form } %> +
+
+ Recipient Address + <%= render partial: "addresses/nested_form", locals: { form: form } %> +
+
+ Return Address +
+
+ <%= form.label :return_address_id, "Select Return Address" %> + <%= form.collection_select :return_address_id, + ReturnAddress.shared.or(ReturnAddress.owned_by(current_user)), + :id, + :display_name, + { prompt: "Select a return address...", selected: letter.return_address_id }, + { class: "form-control", data: { controller: "return-address" } } %> + + <%= link_to "Manage Return Addresses", return_addresses_path(from_letter: true) %> + +
+
+ <%= form.label :return_address_name, "Custom Return Address Name (optional)", class: "form-label" %> + <%= form.text_field :return_address_name, class: "form-control", placeholder: "Leave blank to use the return address name" %> +
+
+
+
+ USPS Mailer ID +
+
+ <%= form.label :usps_mailer_id_id, "Mailer ID" %> + <%= form.collection_select :usps_mailer_id_id, + USPS::MailerId.all, + :id, + :name, + { prompt: "Select a mailer ID...", selected: USPS::MailerId.first&.id }, + { class: "form-control" } %> +
+
+ +
+
+ Additional Data +
+
+ <%= form.label :recipient_email, "Recipient Email", class: "form-label" %> + <%= form.email_field :recipient_email, class: "form-control", placeholder: "recipient@example.com" %> + Optional email address for the recipient +
+
+ <%= form.label :rubber_stamps, "Rubber Stamps" %> + <%= form.text_area :rubber_stamps, rows: 5 %> + Extra text to print on the label! +
+
+ <%= render 'shared/tag_picker', form: form %> +
+ +
+
+ <%= form.submit "do it!", class: "success" %> + <%= link_to "cancel", letters_path, class: "btn btn-tiny danger outlined" %> +
+
+<% end %> +<%= javascript_tag do %> + document.addEventListener('DOMContentLoaded', function() { + const returnAddressSelect = document.querySelector('#letter_return_address_id'); + const postageOptions = document.querySelector('#postage-options'); + const stampsRadio = document.querySelector('input[name="letter[postage_type]"][value="stamps"]'); + const indiciaRadio = document.querySelector('input[name="letter[postage_type]"][value="indicia"]'); + // Store country data in a data attribute when rendering the select options + const returnAddresses = <%= raw ReturnAddress.shared.or(ReturnAddress.owned_by(current_user)).map { |ra| { id: ra.id, country: ra.country } }.to_json %>; + function updatePostageTypeOptions() { + const selectedId = returnAddressSelect.value; + if (!selectedId) return; + const selectedAddress = returnAddresses.find(ra => ra.id.toString() === selectedId); + const isUS = selectedAddress?.country === 'US'; + if (isUS) { + postageOptions.style.display = 'block'; + stampsRadio.checked = true; + } else { + postageOptions.style.display = 'none'; + // Set postage type to international_origin for non-US addresses + const internationalOriginInput = document.createElement('input'); + internationalOriginInput.type = 'hidden'; + internationalOriginInput.name = 'letter[postage_type]'; + internationalOriginInput.value = 'international_origin'; + postageOptions.appendChild(internationalOriginInput); + } + } + returnAddressSelect.addEventListener('change', updatePostageTypeOptions); + // Initial check + updatePostageTypeOptions(); + }); +<% end %> diff --git a/app/views/letters/_letter.html.erb b/app/views/letters/_letter.html.erb new file mode 100644 index 0000000..90a4841 --- /dev/null +++ b/app/views/letters/_letter.html.erb @@ -0,0 +1,36 @@ +
+
+

+ <%= link_to letter do %> + <% if letter.user_facing_title.present? %> + <%= letter.user_facing_title %> + <% else %> + <%= letter.public_id %> to <%= letter.address.name_line %> + <% end %> + <% end %> +

+ <%= letter_status_badge(letter.aasm_state, 'float-right') %> +
+ +
+
+ From: + <%= letter.return_address.present? ? letter.return_address_name_line : 'N/A' %> +
+ +
+ Created: + <%= time_ago_in_words(letter.created_at) %> +
+ +
+ Size: + <%= letter.height %>" × <%= letter.width %>" +
+
+ +
+ Tags: + <%= render 'shared/tags', tags: letter.tags %> +
+
diff --git a/app/views/letters/_letter_attributes_form.html.erb b/app/views/letters/_letter_attributes_form.html.erb new file mode 100644 index 0000000..69cb980 --- /dev/null +++ b/app/views/letters/_letter_attributes_form.html.erb @@ -0,0 +1,73 @@ + <%# Letter attributes form partial that can be used for both batches and individual letters %> + <%# Usage: render 'letters/letter_attributes_form', form: form, is_batch: true/false %> + <% is_batch ||= false %> +
+
+
+ <%= form.label is_batch ? :letter_width : :width, "Letter Width (inches)", class: "form-label" %> + <%= form.number_field is_batch ? :letter_width : :width, + step: "0.5", + min: 0, + id: "letter_width", + class: "form-control" %> +
+
+ <%= form.label is_batch ? :letter_height : :height, "Letter Height (inches)", class: "form-label" %> + <%= form.number_field is_batch ? :letter_height : :height, + step: "0.5", + min: 0, + id: "letter_height", + class: "form-control" %> +
+
+ <%= form.label is_batch ? :letter_weight : :weight, "Letter Weight (ounces)", class: "form-label" %> + <%= form.number_field is_batch ? :letter_weight : :weight, + step: "0.1", + min: 0, + value: 1, + class: "form-control" %> +
+ +
+ <%= form.label is_batch ? :letter_processing_category : :processing_category, "Processing Category", class: "form-label" %> + <%= form.select is_batch ? :letter_processing_category : :processing_category, + Letter.processing_categories.keys.map { |k| [k.humanize, k.to_sym] }, + { selected: :letter }, + { class: "form-control", required: true } %> +
+ + <% unless local_assigns[:is_batch] %> +
+ <%= form.label :mailing_date, class: "form-label" %> + <%= form.date_field :mailing_date, + value: form.object.mailing_date || form.object.default_mailing_date, + min: form.object.new_record? ? Date.current : nil, + class: "form-control" %> +
+ <% end %> +
+ +
+
Choose a preset size:
+
+ + +
+
+
+ + \ No newline at end of file diff --git a/app/views/letters/_letter_preview.html.erb b/app/views/letters/_letter_preview.html.erb new file mode 100644 index 0000000..862ab5d --- /dev/null +++ b/app/views/letters/_letter_preview.html.erb @@ -0,0 +1,51 @@ +
+ <% if @letter.pending? %> +
PENDING
+ <% end %> + +
+
Theseus Mail Service
+
Letter #<%= letter.public_id %>
+
+ +
+
+
From
+
+ <% if letter.return_address.present? %> + <%= letter.return_address_name_line %>
+ <%= letter.return_address.line_1 %>
+ <% if letter.return_address.line_2.present? %> + <%= letter.return_address.line_2 %>
+ <% end %> + <%= letter.return_address.city %>, <%= letter.return_address.state %> <%= letter.return_address.postal_code %>
+ <%= letter.return_address.country %> + <% else %> + No return address specified + <% end %> +
+
+ +
+
To
+
+ <%= letter.address.name_line %>
+ <%= letter.address.line_1 %>
+ <% if letter.address.line_2.present? %> + <%= letter.address.line_2 %>
+ <% end %> + <%= letter.address.city %>, <%= letter.address.state %> <%= letter.address.postal_code %>
+ <%= letter.address.country %> +
+
+
+ +
+

This is a mail piece being processed through the Theseus Mail Service.

+

It will be delivered to the recipient address shown above.

+
+ + +
\ No newline at end of file diff --git a/app/views/letters/buy_indicia.html.erb b/app/views/letters/buy_indicia.html.erb new file mode 100644 index 0000000..d0f86d6 --- /dev/null +++ b/app/views/letters/buy_indicia.html.erb @@ -0,0 +1,46 @@ +
+

Buy Indicia for Letter #<%= @letter.id %>

+ +
+
+
+
+ Recipient: +
<%= @letter.address.display_name %>
+
<%= @letter.address.line_1 %>
+ <% if @letter.address.line_2.present? %> +
<%= @letter.address.line_2 %>
+ <% end %> +
<%= @letter.address.city %>, <%= @letter.address.state %> <%= @letter.address.postal_code %>
+
<%= @letter.address.country %>
+
+
+ Dimensions: +
<%= @letter.height %> × <%= @letter.width %> in
+ Weight: +
<%= @letter.weight %> oz
+ <% if @letter.non_machinable? %> +
Non-machinable
+ <% end %> +
+
+ + <%= form_with(url: buy_indicia_letter_path(@letter), method: :post, class: "form-container") do |form| %> +
+ <%= form.label :usps_payment_account_id, "USPS Payment Account", class: "form-label" %> + <%= form.collection_select :usps_payment_account_id, + USPS::PaymentAccount.all, + :id, + :name, + { prompt: "Select a payment account", selected: USPS::PaymentAccount.first&.id }, + { class: "form-control", required: true } %> +
+ +
+ <%= form.submit "Buy Indicia", class: "btn btn-primary" %> + <%= link_to "Cancel", @letter, class: "btn btn-secondary" %> +
+ <% end %> +
+
+
\ No newline at end of file diff --git a/app/views/letters/edit.html.erb b/app/views/letters/edit.html.erb new file mode 100644 index 0000000..7157d46 --- /dev/null +++ b/app/views/letters/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing letter" %> + +

Editing letter

+ +<%= render "form", letter: @letter %> + +
+ +
+ <%= link_to "Show this letter", @letter %> | + <%= link_to "Back to letters", letters_path %> +
diff --git a/app/views/letters/index.html.erb b/app/views/letters/index.html.erb new file mode 100644 index 0000000..51df5ee --- /dev/null +++ b/app/views/letters/index.html.erb @@ -0,0 +1,72 @@ +<% content_for :title, "Letters" %> + +
+
+
+

Letters

+
+
+ <%= create_button new_letter_path, "send a letter!" %> +
+
+
+ + + +
+ <% if params[:view] == 'batched' %> +
+

Batched Letters

+
+ <% if @batched_letters.any? %> + <% @batched_letters.each do |batch, letters| %> +
+
+ +
+ Batch #<%= batch.id %> + <%= pluralize(letters.size, 'letter') %> +
+ Created <%= time_ago_in_words(batch.created_at) %> ago +
+
+ Tags: + <%= render 'shared/tags', tags: batch.tags %> +
+
+
+
+ <%= render partial: 'batches/letters_collection', locals: { letters: letters } %> +
+
+
+ <% end %> + <% else %> +

No batched letters found.

+ <% end %> + <% else %> +
+

Unbatched Letters

+
+ <% if @unbatched_letters.any? %> + <%= render partial: 'batches/letters_collection', locals: { letters: @unbatched_letters } %> + <% else %> +

No unbatched letters found.

+ <% end %> + <% end %> +
+ +<% if params[:view] != 'batched' %> +
+ <%= paginate @unbatched_letters %> +
+<% end %> \ No newline at end of file diff --git a/app/views/letters/index.json.jbuilder b/app/views/letters/index.json.jbuilder new file mode 100644 index 0000000..45c84ae --- /dev/null +++ b/app/views/letters/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @letters, partial: "letters/letter", as: :letter diff --git a/app/views/letters/new.html.erb b/app/views/letters/new.html.erb new file mode 100644 index 0000000..b3e1533 --- /dev/null +++ b/app/views/letters/new.html.erb @@ -0,0 +1,30 @@ +<% content_for :title, "New Letter" %> + +<%= render partial: "shared/page_header", locals: { + title: "Create New Letter", + actions: link_to("Back to Letters", letters_path, class: "btn btn-secondary") +} %> + +
+
+ <%= render "form", letter: @letter %> +
+ +
+
+
+

About Letters

+
+
+

this should be nicer than Dymo Label!

+ +
+

Helpful Tips

+
    +
  • uuuhhh idk make sure you use enough stamps
  • +
+
+
+
+
+
diff --git a/app/views/letters/show.html.erb b/app/views/letters/show.html.erb new file mode 100644 index 0000000..8179113 --- /dev/null +++ b/app/views/letters/show.html.erb @@ -0,0 +1,256 @@ +<% content_for :title, "#{@letter.user_facing_title || 'Letter'} #{@letter.public_id} - #{@letter.address.name_line}" %> +
+ +
+
+
+
+
+

Details

+ <%= link_to "Show on public page", public_letter_path(@letter), target: "_blank" %> +
+
+ <%= render 'shared/tags', tags: @letter.tags %> +
+
+

From

+ <% if @letter.return_address.present? %> +
+
<%= @letter.return_address_name_line %>
+
<%= @letter.return_address.line_1 %>
+ <% if @letter.return_address.line_2.present? %> +
<%= @letter.return_address.line_2 %>
+ <% end %> +
<%= @letter.return_address.city %>, <%= @letter.return_address.state %> <%= @letter.return_address.postal_code %>
+
<%= @letter.return_address.country %>
+
+ <% else %> +

No return address

+ <% end %> +
+
+

To

+
+
<%= @letter.address.name_line %>
+
<%= @letter.address.line_1 %>
+ <% if @letter.address.line_2.present? %> +
<%= @letter.address.line_2 %>
+ <% end %> +
<%= @letter.address.city %>, <%= @letter.address.state %> <%= @letter.address.postal_code %>
+
<%= @letter.address.country %>
+
+
+
+

Specifications

+ <%= render 'shared/letter_attributes', record: @letter %> +

Postage Information

+
+
+ <% if @letter.postage_type == "indicia" %> + <% if @letter.usps_indicium.present? %> + <%= render 'admin_inspector', record: @letter.usps_indicium %> +
+
+ Postage Type: + Indicia (Metered) +
+
+ Cost: + <%= number_to_currency(@letter.usps_indicium.cost) %> + (<%= number_to_currency(@letter.usps_indicium.postage) %> postage + <%= number_to_currency(@letter.usps_indicium.fees || 0) %> fees) +
+
+ Mailing Date: + <%= @letter.usps_indicium.mailing_date %> +
+
+ USPS SKU: + <%= @letter.usps_indicium.usps_sku %> +
+
+ <% else %> +

+ Postage Type: + Indicia (Metered) - Not Purchased Yet +

+ <% if @letter.batch_id.nil? %> +
+ <%= form_with(url: buy_indicia_letter_path(@letter), method: :post) do |form| %> +
+ <%= form.collection_select :usps_payment_account_id, + USPS::PaymentAccount.all, + :id, + :name, + { prompt: "Select a payment account", selected: USPS::PaymentAccount.first&.id }, + { required: true } %> +
+ <%= form.submit "Buy Indicia", class: "primary" %> + <% end %> +
+ <% end %> + <% end %> + <% elsif @letter.postage_type == "international_origin" %> +

+ Postage Type: + International Origin +

+
+
+ Cost: + whatever <%= @letter.return_address.country %> charges to send a letter to <%= @letter.address.country %> +
+
+ <% else %> +

+ Postage Type: + Stamps +

+ <% if @letter.batch_id.nil? %> +
+ <%= button_to "Switch to Indicia", letter_path(@letter), method: :patch, params: { letter: { postage_type: "indicia" } }, class: "primary" %> +
+ <% end %> + <% end %> +
+
+ <% if @letter.batch.present? %> + <%= render 'shared/batch_info', record: @letter %> + <% end %> + <% if @letter.imb_serial_number.present? && @letter.address.country&.upcase == 'US' %> +

IMb Information

+
+
+ Serial: + <%= @letter.imb_serial_number.to_s.rjust(6, '0') %> +
+
+ Epoch: + <%= @letter.imb_rollover_count %> +
+
+ <% end %> + <% if @letter.rubber_stamps.present? %> +
+
Rubber Stamps
+
<%= @letter.rubber_stamps %>
+
+ <% end %> +
+
+ <%= render partial: "admin_inspector", locals: {record: @letter} %> +
+
+
+
+

Actions

+
+
+ <% case @letter.aasm_state %> + <% when 'pending' %> + <% if @letter.label.attached? %> +
+

Label

+
+ <%= link_to "View Label", rails_blob_path(@letter.label, disposition: 'inline'), class: "primary", target: "_blank" %> +
+
+ <%= button_to "Clear Label", clear_label_letter_path(@letter), method: :post, class: "contrast", data: { confirm: "Are you sure you want to clear this label?" } %> +
+
+ <% end %> + <% if @letter.batch.present? || @letter.label.attached? %> +
+

Actions

+
+ <%= button_to mark_printed_letter_path(@letter), method: :post, class: "btn btn-sm success", id: 'mark_printed' do %> + + + + mark printed! + <% end %> +
+
+ <% elsif !@letter.batch.present? %> +
+

Generate Label

+ <%= form_tag(generate_label_letter_path(@letter), method: :post) do %> +
+ <%= render 'shared/template_picker', multiple: false %> + +
+
+ +
+ + <% end %> + <% dev_tool do %> + <%= link_to "preview label?", preview_template_letter_path(@letter) %> + <% end %> +
+ <% end %> + <% when 'printed' %> +
+

Label

+ <%= button_to mark_mailed_letter_path(@letter), method: :post, class: "btn btn-sm success" do %> + + + + mark as mailed! + <% end %> +
+ <% when 'mailed' %> +
+

Label

+ <%= button_to "Mark as Received", mark_received_letter_path(@letter), method: :post, class: "success" %> +
+ <% when 'received' %> +
+

Label

+

Letter has been received.

+
+ <% end %> +
+
+ <% if @letter.label.attached? %> + <%= render 'shared/instant_print_window', url: rails_blob_path(@letter.label, disposition: 'inline') %> +
+
+

Label Preview

+
+
+
+ <%= link_to "Download Label", rails_blob_path(@letter.label, disposition: "attachment"), class: "secondary" %> +
+
+ +
+
+
+ <% end %> +
+
+
+
diff --git a/app/views/letters/show.json.jbuilder b/app/views/letters/show.json.jbuilder new file mode 100644 index 0000000..3cfeee2 --- /dev/null +++ b/app/views/letters/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "letters/letter", letter: @letter diff --git a/app/views/public/_backend_controls.html.erb b/app/views/public/_backend_controls.html.erb new file mode 100644 index 0000000..f9abfc7 --- /dev/null +++ b/app/views/public/_backend_controls.html.erb @@ -0,0 +1,31 @@ +<% if current_user %> +
+
+
backend controls >:3
+
+
+

backend: you're logged in as <%= current_user.username %>

+
+

Go to <%= link_to "back office", root_path %>?

+ <% if content_for? :back_office_link %> +

<%= yield :back_office_link %>

+ <% end %> + <% if policy(Public::Impersonation).new? %> +

+ <% if impersonating? %> + currently impersonating <%= current_public_user.email %> + . <%= link_to "stop", public_stop_impersonating_path %>? + <% else %> + <%= link_to "impersonate", public_impersonate_form_path %> somebody else? + <% end %> +

+ <% end %> +

+ <%= copy_to_clipboard(Rails.application.config.git_version, label: "running rev #{Rails.application.config.git_version}", tooltip_direction: "e") do %> + <%= Rails.env.humanize %> mode + <% end %> +

+
+
+
+<% end %> \ No newline at end of file diff --git a/app/views/public/api/v1/letters/_letter.jb b/app/views/public/api/v1/letters/_letter.jb new file mode 100644 index 0000000..f0e4e79 --- /dev/null +++ b/app/views/public/api/v1/letters/_letter.jb @@ -0,0 +1,12 @@ +{ + id: letter.public_id, + type: :letter, + path: if_expand(:path) { public_v1_letter_path(letter) }, + public_url: public_letter_url(letter), + status: letter.aasm_state, + tags: letter.tags || [], + title: letter.user_facing_title, + created_at: letter.created_at, + updated_at: letter.updated_at, + events: if_expand(:events) { letter.events }, +}.compact diff --git a/app/views/public/api/v1/letters/index.jb b/app/views/public/api/v1/letters/index.jb new file mode 100644 index 0000000..9c1da9c --- /dev/null +++ b/app/views/public/api/v1/letters/index.jb @@ -0,0 +1,5 @@ +{ + letters: expand(:path) do + render @letters || [] + end, +} diff --git a/app/views/public/api/v1/letters/show.jb b/app/views/public/api/v1/letters/show.jb new file mode 100644 index 0000000..532728b --- /dev/null +++ b/app/views/public/api/v1/letters/show.jb @@ -0,0 +1,3 @@ +{ + letter: expand(:events) { render(@letter) }, +} diff --git a/app/views/public/api/v1/lsv/index.jb b/app/views/public/api/v1/lsv/index.jb new file mode 100644 index 0000000..11fb825 --- /dev/null +++ b/app/views/public/api/v1/lsv/index.jb @@ -0,0 +1,3 @@ +{ + legacy_shipment_viewer_records: expand(:path) { @lsv.map { |l| render(l) } || [] }, +} diff --git a/app/views/public/api/v1/lsv/show.jb b/app/views/public/api/v1/lsv/show.jb new file mode 100644 index 0000000..dd23225 --- /dev/null +++ b/app/views/public/api/v1/lsv/show.jb @@ -0,0 +1,3 @@ +{ + legacy_shipment_viewer_record: render(@lsv), +} diff --git a/app/views/public/api/v1/lsv/type/_base.jb b/app/views/public/api/v1/lsv/type/_base.jb new file mode 100644 index 0000000..819b081 --- /dev/null +++ b/app/views/public/api/v1/lsv/type/_base.jb @@ -0,0 +1,22 @@ +slug = LSV.slug_for(base) +json = { + id: base.id, + type: :legacy_shipment_viewer_record, + subtype: base.class.name.underscore.split("/").last, + path: if_expand(:path) { public_v1_lsv_path(slug, base.id) }, + public_url: show_lsv_url(slug, base.id), + original_id: base.source_id, + created_at: base.created_at, + title: base.title_text, + status: base.status_text, + tracking_number: base.tracking_number, + tracking_link: base.tracking_link, +} + +if base.description.is_a?(Array) + json[:contents] = base.description +else + json[:description] = base.description +end + +json.compact diff --git a/app/views/public/api/v1/lsv/type/_msr.jb b/app/views/public/api/v1/lsv/type/_msr.jb new file mode 100644 index 0000000..82dd630 --- /dev/null +++ b/app/views/public/api/v1/lsv/type/_msr.jb @@ -0,0 +1,3 @@ +{ + type: :marketing_shipment_request, +} diff --git a/app/views/public/api/v1/lsv/type/_printful_shipment.jb b/app/views/public/api/v1/lsv/type/_printful_shipment.jb new file mode 100644 index 0000000..dc5126a --- /dev/null +++ b/app/views/public/api/v1/lsv/type/_printful_shipment.jb @@ -0,0 +1,16 @@ +slug = LSV.slug_for(printful_shipment) +{ + id: printful_shipment.source_id, + type: :legacy_shipment_viewer_record, + subtype: :printful_shipment, + path: if_expand(:path) { public_v1_lsv_path(slug, printful_shipment.id) }, + public_url: show_lsv_url(slug, printful_shipment.id), + original_id: printful_shipment.source_id, + created_at: printful_shipment.created_at, + title: printful_shipment.title_text, + status: printful_shipment.status_text, + tracking_number: printful_shipment.tracking_number, + tracking_link: printful_shipment.tracking_link, + contents: printful_shipment.description&.map(&:strip), + order_id: printful_shipment.fields["%order:pretty_id"], +}.compact diff --git a/app/views/public/api/v1/mail/index.json.jb b/app/views/public/api/v1/mail/index.json.jb new file mode 100644 index 0000000..9e1c8f0 --- /dev/null +++ b/app/views/public/api/v1/mail/index.json.jb @@ -0,0 +1,7 @@ +{ + mail: expand(:path) do + @mail.map do |m| + render m + end + end, +} diff --git a/app/views/public/api/v1/packages/index.jb b/app/views/public/api/v1/packages/index.jb new file mode 100644 index 0000000..cfb815a --- /dev/null +++ b/app/views/public/api/v1/packages/index.jb @@ -0,0 +1,3 @@ +{ + packages: expand(:path) { render(@packages) || [] }, +} diff --git a/app/views/public/api/v1/public/users/_user.jb b/app/views/public/api/v1/public/users/_user.jb new file mode 100644 index 0000000..6ffd621 --- /dev/null +++ b/app/views/public/api/v1/public/users/_user.jb @@ -0,0 +1,3 @@ +{ + id: user.public_id +} \ No newline at end of file diff --git a/app/views/public/api/v1/users/show.jb b/app/views/public/api/v1/users/show.jb new file mode 100644 index 0000000..c1db6d0 --- /dev/null +++ b/app/views/public/api/v1/users/show.jb @@ -0,0 +1,3 @@ +{ + user: render(@user), +} diff --git a/app/views/public/api/v1/warehouse/line_items/_line_item.jb b/app/views/public/api/v1/warehouse/line_items/_line_item.jb new file mode 100644 index 0000000..b002db6 --- /dev/null +++ b/app/views/public/api/v1/warehouse/line_items/_line_item.jb @@ -0,0 +1,5 @@ +{ + hc_sku: line_item.sku.sku, + name: line_item.sku.name, + quantity: line_item.quantity, +} diff --git a/app/views/public/api/v1/warehouse/orders/_order.jb b/app/views/public/api/v1/warehouse/orders/_order.jb new file mode 100644 index 0000000..4609e92 --- /dev/null +++ b/app/views/public/api/v1/warehouse/orders/_order.jb @@ -0,0 +1,20 @@ +{ + id: order.hc_id, + type: :warehouse_order, + path: if_expand(:path) { public_v1_package_path(order) }, + public_url: public_package_url(order), + status: order.aasm_state, + tags: order.tags || [], + title: order.user_facing_title, + description: order.user_facing_description, + created_at: order.created_at, + updated_at: order.updated_at, + dispatched_at: order.dispatched_at, + mailed_at: order.mailed_at, + tracking_number: order.tracking_number, + tracking_link: order.tracking_url, + carrier: order.carrier, + service: order.service, + weight: order.weight, + contents: order.surprise? ? [] : (render(order.line_items) || []), +}.compact diff --git a/app/views/public/api_keys/_api_key.html.erb b/app/views/public/api_keys/_api_key.html.erb new file mode 100644 index 0000000..d51182a --- /dev/null +++ b/app/views/public/api_keys/_api_key.html.erb @@ -0,0 +1,14 @@ + + + <%= link_to api_key.name, public_api_key_path(api_key) %> + + + <%= api_key.abbreviated %> + + + <%= api_key.created_at.strftime("%b %d, %Y") %> + + + <%= api_key.revoked? ? "Revoked" : "Active" %> + + \ No newline at end of file diff --git a/app/views/public/api_keys/index.html.erb b/app/views/public/api_keys/index.html.erb new file mode 100644 index 0000000..d44076a --- /dev/null +++ b/app/views/public/api_keys/index.html.erb @@ -0,0 +1,32 @@ +
+
+
API Keys
+
+ <%= w95_title_button_to("Close", public_root_path) %> +
+
+
+ <% if @api_keys&.any? %> +
+ + + + + + + + + + + <%= render @api_keys %> + +
NameTokenCreatedStatus
+
+ <% else %> + You don't have any API keys yet.
+ Would you like to change that? + <% end %> +
+ <%= button_to "Create API Key", new_public_api_key_path, method: :get, class: "default" %> +
+
\ No newline at end of file diff --git a/app/views/public/api_keys/new.html.erb b/app/views/public/api_keys/new.html.erb new file mode 100644 index 0000000..3d31c68 --- /dev/null +++ b/app/views/public/api_keys/new.html.erb @@ -0,0 +1,24 @@ +
+
+
Create API Key
+
+ <%= w95_title_button_to("Close", public_root_path) %> +
+
+
+ <%= form_with model: @api_key, url: public_api_keys_path do |f| %> +
+ <%= f.label :name, "Name:" %> + <%= f.text_field :name, placeholder: ["Letter Lynx", "Package Panda", "Mail Mouse", "Letter Lemur"].sample %> +
+ + Just for your reference.
+ Maybe something short and descriptive of where you'll use it? +

+
+
+ <%= f.submit "Create API Key", class: "default" %> +
+ <% end %> +
+
\ No newline at end of file diff --git a/app/views/public/api_keys/revoke_confirm.html.erb b/app/views/public/api_keys/revoke_confirm.html.erb new file mode 100644 index 0000000..a7d3369 --- /dev/null +++ b/app/views/public/api_keys/revoke_confirm.html.erb @@ -0,0 +1,15 @@ +
+
+
Are you sure?
+
+ <%= w95_title_button_to("Close", public_api_keys_path) %> +
+
+
+ Are you sure you want to revoke the API key "<%= @api_key.name %>"? +

This action cannot be undone, and everything that uses this key will stop working.

+
+ <%= button_to "i understand, do it.", revoke_public_api_key_path(@api_key), method: :post, class: "default" %> +
+
+
\ No newline at end of file diff --git a/app/views/public/api_keys/show.html.erb b/app/views/public/api_keys/show.html.erb new file mode 100644 index 0000000..59c4155 --- /dev/null +++ b/app/views/public/api_keys/show.html.erb @@ -0,0 +1,30 @@ +
+
+
API Key: <%= @api_key.name %>
+
+ <%= w95_title_button_to("Close", public_api_keys_path) %> +
+
+
+
+ Created: + <%= @api_key.created_at %> +
+
+ Status: + <%= @api_key.revoked? ? "Revoked" : "Active" %> +
+ <% if @api_key.revoked? %> +
+ Revoked: + <%= @api_key.revoked_at %> +
+ <% end %> +
+ key (click to copy): + <%= copy_to_clipboard(@api_key.token, tooltip_direction: 'e') do %> +
<%= @api_key.token %>
+ <% end %> +
+ <%= button_to "revoke", revoke_confirm_public_api_key_path(@api_key), method: :get, class: "default" %> +
\ No newline at end of file diff --git a/app/views/public/impersonations/new.html.erb b/app/views/public/impersonations/new.html.erb new file mode 100644 index 0000000..9e75c00 --- /dev/null +++ b/app/views/public/impersonations/new.html.erb @@ -0,0 +1,30 @@ +
+
+ <%= vite_image_tag 'images/icons/impersonate.png', id: "treasure", class: 'title-icon' %> + +
Impersonate user
+
+
+ <%= vite_image_tag 'images/icons/break-the-glass.png', id: "treasure", class: 'dialog-icon' %> + +

+ With great power comes great responsibility.
+ Privacy is important when it comes to mail.
+ Please use this tool responsibly.
+

+ + <%= form_with url: public_impersonate_path, model: @impersonation do |f| %> +
+ <%= f.label :target_email, "What's the user's email?" %> + <%= f.text_field :target_email %> +
+
+ <%= f.label :justification, "Why are you accessing their data today?" %> + <%= f.text_field :justification %> +
+

(doesn't need to be anything fancy, just not "for the hell of it" :-P)

+ + <%= f.submit "go undercover" %> + <% end %> +
+
\ No newline at end of file diff --git a/app/views/public/leaderboards/show.html.erb b/app/views/public/leaderboards/show.html.erb new file mode 100644 index 0000000..c443c5f --- /dev/null +++ b/app/views/public/leaderboards/show.html.erb @@ -0,0 +1,18 @@ +<% map = { this_week: "Letters sent this week", this_month: "Letters sent this month", all_time: "Letters sent all-time" } %> +<% map.map do |k, v| %> + <% if @tab == k %> + <%= v %> + <% else %> + <%= link_to(v, url_for(controller: 'public/leaderboards', action: k)) %> + <% end %> +<% end %> + <% @lb.each_with_index do |u, i| %> +

+ #<%= i+1 %>: + + <%= u.username %> – <%= u.letter_count %> +

+ <% end %> +<% unless @lb.any? %> +

no <%= map[@tab].downcase %>? for shame, HQ!

+<% end %> \ No newline at end of file diff --git a/app/views/public/letters/_letter.html.erb b/app/views/public/letters/_letter.html.erb new file mode 100644 index 0000000..3df2fef --- /dev/null +++ b/app/views/public/letters/_letter.html.erb @@ -0,0 +1,28 @@ +<% if false %> +
+
+
Letter <%= letter.public_id %>
+
+ <%= w95_title_button_to "Maximize", public_letter_path(letter) %> +
+
+
+
+ Status: + <%= letter.aasm_state.humanize %> +
+
+
+<% end %> + + + <%= letter.created_at.strftime("%Y-%m-%d") %> + + Letter: <%= letter.display_name %> + + <%= letter.aasm_state.humanize %> + + <%= link_to "Open", public_letter_path(letter), + onclick: "openIframeWindow(this.href, 'Letter: #{letter.display_name}'); return false;", class: 'open-link' %> + + \ No newline at end of file diff --git a/app/views/public/letters/show.html.erb b/app/views/public/letters/show.html.erb new file mode 100644 index 0000000..0d9a67d --- /dev/null +++ b/app/views/public/letters/show.html.erb @@ -0,0 +1,80 @@ +<% content_for :back_office_link do %> + <%= link_to "view this letter", letter_path(@letter) %> in the backend? +<% end %> +<% content_for :window_title do %> + Letter: <%= @letter.display_name %> +<% end %> +<%= copy_to_clipboard(@letter.public_id, tooltip_direction: 'e', label: "#{@letter.public_id} (click to copy)") do %> + (click here to copy letter ID) +<% end %> +

+ Status: + <%= @letter.aasm_state.humanize %> +

+<% tags = @letter.tags.compact_blank %> +<% if tags.any? %> +

+ Tagged: <%= tags.join(', ') %> +

+<% end %> +

+

+ details for nerds +
+ + + + + + + + + + + + + <% @events.each do |event| %> + + + + + + + + + <% end %> + +
TimeDescriptionLocationFacilitySourceExtra info
+ <%= event[:happened_at].strftime("%Y-%m-%d %H:%M EST") %> + + <%= event[:description] %> + + <%= event[:location] %> + + <%= event[:facility] %> + + <%= event[:source] %> + + <%= event[:extra_info] %> +
+
+
+

+<%= render partial: 'application/admin_inspector', locals: { record: @letter } %> +<% if @letter.may_mark_mailed? %> +
+ <%= button_to "MARK MAILED", public_mark_mailed_letter_path(@letter), style: "font-size: 24px; padding: 20px 40px; font-weight: bold;" %> +
+<% end %> +<% if @letter.mailed_at && !@letter.received_at %> +
+ <%= button_to "i got this letter!", public_mark_received_letter_path(@letter), method: :post, form: { data: { turbo_confirm: "are you sure you received this letter?" } } %> +
+<% end %> +<% if @received %> + +<% end %> diff --git a/app/views/public/login_code_mailer/send_login_code.text.erb b/app/views/public/login_code_mailer/send_login_code.text.erb new file mode 100644 index 0000000..a8e1742 --- /dev/null +++ b/app/views/public/login_code_mailer/send_login_code.text.erb @@ -0,0 +1,16 @@ +<% if false %> +<%= { + transactionalId: "cm8jh9gzf0nm6wek854cgi7y6", + email: @recipient, + dataVariables: { + login_code_url: @login_code_url, + } + }.to_json %> +<% end %> + +👋 hey! +here's your link to view your mailing info: +<%= link_to "log in!", @login_code_url %> +it expires in 30 minutes, so be quick! +forever yours, +a dinosaur with a box on its head diff --git a/app/views/public/lsv/customs_receipt.erb b/app/views/public/lsv/customs_receipt.erb new file mode 100644 index 0000000..ca4f077 --- /dev/null +++ b/app/views/public/lsv/customs_receipt.erb @@ -0,0 +1,3 @@ +<%= content_for :window_title, "Generate customs receipt" %> +

This will generate a customs receipt for shipment ID <%= @msr.id %> and email it to you at <%= @msr.email %>.

+<%= button_to "do it!", msr_generate_customs_receipt_path(@msr), method: :post %> \ No newline at end of file diff --git a/app/views/public/lsv/show.html.erb b/app/views/public/lsv/show.html.erb new file mode 100644 index 0000000..72b4062 --- /dev/null +++ b/app/views/public/lsv/show.html.erb @@ -0,0 +1 @@ +<%= render @lsv %> \ No newline at end of file diff --git a/app/views/public/lsv/type/_base.html.erb b/app/views/public/lsv/type/_base.html.erb new file mode 100644 index 0000000..32e3260 --- /dev/null +++ b/app/views/public/lsv/type/_base.html.erb @@ -0,0 +1,80 @@ +<% if local_assigns[:collection] %> + + <%= base.created_at.in_time_zone('America/New_York') %> + + <%= base.type_text %>: <%= base.title_text %> + + <%= base.status_text %> + + <%= link_to "Open", show_lsv_path(LSV.slug_for(base), base.id), + onclick: "openIframeWindow(this.href, '#{base.type_text}: #{base.title_text}'); return false;", + class: 'btn' %> + + +<% else %> +<% content_for :window_title do %> + <%= base.type_text %>: <%= base.title_text %> +<% end %> +
+
+ <%= base.title_text %> +
+ +
+ + <%= base.type_text %> +
+ +
+ + <%= base.status_text %> +
+ + <% if base["Warehouse-Service"] %> +
+ + <%= base["Warehouse–Service"] %> +
+ <% end %> + + <% if base.tracking_number %> +
+ + <% if base.tracking_link %> + <%= link_to(base.tracking_number, base.tracking_link, target: "_blank", class: "link") %> + <% else %> + <%= base.tracking_number %> + <% end %> +
+ <% end %> + + <% if base.description %> +
+ + <% if base.description.is_a? Array %> +
    + <% base.description.each do |item| %> +
  • <%= item %>
  • + <% end %> +
+ <% else %> + <%= base.description %> + <% end %> +
+ <% end %> + +
+ + <%= base.created_at %> +
+ + <% back_office_tool do %> +
+ + <%= link_to(base.source_id, base.source_url, target: "_blank") %> +
+ <% end %> + + <%= render 'application/admin_inspector', record: base %> +
+<% end %> diff --git a/app/views/public/lsv/type/_msr.html.erb b/app/views/public/lsv/type/_msr.html.erb new file mode 100644 index 0000000..a70e52c --- /dev/null +++ b/app/views/public/lsv/type/_msr.html.erb @@ -0,0 +1,77 @@ +<% if local_assigns[:collection] %> + + <%= msr.created_at.in_time_zone('America/New_York') %> + + <%= msr.type_text %>: <%= msr.title_text %> + + <%= msr.status_text %> + + <%= link_to "Open", show_lsv_path(LSV.slug_for(msr), msr.id), + onclick: "openIframeWindow(this.href, '#{msr.type_text}: #{msr.title_text}'); return false;", + class: 'btn' %> + + +<% else %> + <% content_for :window_title do %> + <%= msr.type_text %>: <%= msr.title_text %> + <% end %> +
+
+ <%= msr.title_text %> +
+
+ + <%= msr.type_text %> +
+
+ + <%= msr.status_text %> +
+ <% if msr["Warehouse-Service"] %> +
+ + <%= msr["Warehouse–Service"] %> +
+ <% end %> + <% if msr.tracking_number %> +
+ + <% if msr.tracking_link %> + <%= link_to(msr.tracking_number, msr.tracking_link, target: "_blank", class: "link") %> + <% else %> + <%= msr.tracking_number %> + <% end %> +
+ <% end %> + <% if msr.description %> +
+ + <% if msr.description.is_a? Array %> +
    + <% msr.description.each do |item| %> +
  • <%= item %>
  • + <% end %> +
+ <% else %> + <%= msr.description %> + <% end %> +
+ <% end %> +
+ + <%= msr.created_at %> +
+ <% back_office_tool do %> +
+ + <%= link_to(msr.source_id, msr.source_url, target: "_blank") %> +
+ <% end %> + <% if current_public_user&.email == msr.email && msr.country != "US" %> +
+ <%= button_to "generate customs receipt", msr_customs_receipt_path(msr), method: :get %> +
+ <% end %> + <%= render 'application/admin_inspector', record: msr %> +
+<% end %> \ No newline at end of file diff --git a/app/views/public/lsv/type/_printful_shipment.html.erb b/app/views/public/lsv/type/_printful_shipment.html.erb new file mode 100644 index 0000000..a715fbd --- /dev/null +++ b/app/views/public/lsv/type/_printful_shipment.html.erb @@ -0,0 +1,82 @@ +<% if local_assigns[:collection] %> + + <%= printful_shipment.created_at.in_time_zone('America/New_York') %> + + <%= printful_shipment.type_text %>: <%= printful_shipment.title_text %> + + <%= printful_shipment.status_text %> + + <%= link_to "Open", show_lsv_path(LSV.slug_for(printful_shipment), printful_shipment.id), + onclick: "openIframeWindow(this.href, '#{printful_shipment.type_text}: #{printful_shipment.title_text}'); return false;", + class: 'btn' %> + + +<% else %> + <% content_for :window_title do %> + <%= printful_shipment.type_text %>: <%= printful_shipment.title_text %> + <% end %> +
+
+ <%= printful_shipment.title_text %> +
+
+ + <%= printful_shipment.type_text %> +
+
+ + <%= printful_shipment.status_text %> +
+ <% if printful_shipment.tracking_number %> +
+ + <% if printful_shipment.tracking_link %> + <%= link_to(printful_shipment.tracking_number, printful_shipment.tracking_link, target: "_blank", class: "link") %> + <% else %> + <%= printful_shipment.tracking_number %> + <% end %> +
+ <% end %> + <% if printful_shipment.description %> +
+ + <% if printful_shipment.description.is_a? Array %> +
    + <% printful_shipment.description.each do |item| %> +
  • <%= item %>
  • + <% end %> +
+ <% else %> + <%= printful_shipment.description %> + <% end %> +
+ <% end %> +
+ + <%= printful_shipment.created_at %> +
+ <% back_office_tool do %> + <% if printful_shipment.fields["%order:pretty_id"] %> +
+ + + <%= link_to printful_shipment.fields["%order:pretty_id"], printful_shipment.fields["%order:dashboard_url"], target: "_blank", class: "link" %> + +
+ <% end %> + <% if printful_shipment.fields["packing_slip_url"] %> +
+ + + <%= link_to "View Packing Slip", printful_shipment.fields["packing_slip_url"], target: "_blank", class: "link" %> + +
+ <% end %> +
+ + <%= link_to(printful_shipment.source_id, printful_shipment.source_url, target: "_blank") %> +
+ <% end %> + <%= render 'application/admin_inspector', record: printful_shipment %> +
+<% end %> \ No newline at end of file diff --git a/app/views/public/mail/index.html.erb b/app/views/public/mail/index.html.erb new file mode 100644 index 0000000..ff62a22 --- /dev/null +++ b/app/views/public/mail/index.html.erb @@ -0,0 +1,41 @@ +<% if @mail.any? %> +
+
+
the mail
+
+ <%= w95_title_button_to("Close", public_root_path) %> +
+
+
+ here's your mail: +
+ + + + + + + + + + + <%= render @mail, collection: true %> + +
Date CreatedMailpieceStatusOpen
+
+
+
+<% else %> +
+
+
NO MAIL
+
+ <%= w95_title_button_to("Close", public_root_path) %> +
+
+
+ <%= vite_image_tag 'images/no_mail.png', id: "no_mail" %> +

we don't have any mail for you in the new system...
check back later?

+
+
+<% end %> \ No newline at end of file diff --git a/app/views/public/maps/show.html.erb b/app/views/public/maps/show.html.erb new file mode 100644 index 0000000..8371a54 --- /dev/null +++ b/app/views/public/maps/show.html.erb @@ -0,0 +1,131 @@ +<% content_for :window_title, "Letter Tracking Map" %> +
+ + + Current Location + + + + Traveled Path + + + + Projected Path + +
+
+

(coords not exact for obvious reasons)

+ + + + + \ No newline at end of file diff --git a/app/views/public/packages/customs_receipt.html.erb b/app/views/public/packages/customs_receipt.html.erb new file mode 100644 index 0000000..ee537f9 --- /dev/null +++ b/app/views/public/packages/customs_receipt.html.erb @@ -0,0 +1,3 @@ +<%= content_for :window_title, "Generate customs receipt" %> +

This will generate a customs receipt for package <%= @package.hc_id %> and email it to you at <%= @package.recipient_email %>.

+<%= button_to "do it!", package_generate_customs_receipt_path(@package), method: :post %> \ No newline at end of file diff --git a/app/views/public/sessions/send_email.html.erb b/app/views/public/sessions/send_email.html.erb new file mode 100644 index 0000000..79463ec --- /dev/null +++ b/app/views/public/sessions/send_email.html.erb @@ -0,0 +1,15 @@ +
+
+
Logging in...
+
+ <%= w95_title_button_to("Close", public_root_path) %> +
+
+
+

Email sent to <%= @email %>!

+

Click the link in it to log in...

+ <% dev_tool do %> + you're in dev! go check <%= link_to "letter opener", letter_opener_web_path %>! + <% end %> +
+
\ No newline at end of file diff --git a/app/views/public/static_pages/login.html.erb b/app/views/public/static_pages/login.html.erb new file mode 100644 index 0000000..726614e --- /dev/null +++ b/app/views/public/static_pages/login.html.erb @@ -0,0 +1,20 @@ +
+
+
Log in
+
+ <%= w95_title_button_to("Close", public_root_path) %> +
+
+
+ <%= form_with url: send_email_path do |form| %> +
+ <%= form.label :email, "What's your email? " %> + <%= form.text_field :email %> +
+ <% if @error %> +
<%= @error %>

+ <% end %> + <%= form.submit "log in!" %> + <% end %> +
+
\ No newline at end of file diff --git a/app/views/public/static_pages/root.html.erb b/app/views/public/static_pages/root.html.erb new file mode 100644 index 0000000..4704a61 --- /dev/null +++ b/app/views/public/static_pages/root.html.erb @@ -0,0 +1,47 @@ +
+
+
Hack Club Mail!
+
+
+

there's not a whole lot here yet?

+ <% if current_public_user %> +

you're currently signed in as <%= current_public_user.email %>

+ Would you like to... +
    +
  • +

    view <%= link_to "your mail", my_mail_path %>?

    +
  • +
  • + set up an <%= link_to "API key", public_api_keys_path %>? +
  • +
+ <%= button_to "log out?", public_logout_path, { method: :delete } %> + <% else %> +

if you're interested in your mail in particular, you can always...

+ <%= link_to public_login_path do %> + + <% end %> + <% end %> +
+
+
+
+
Letterboard
+
+
+ +
+
+
+
+
Map
+
+ <%= w95_title_button_to("Maximize", map_path) %> +
+
+
+ +
+
\ No newline at end of file diff --git a/app/views/public/warehouse/orders/_order.html.erb b/app/views/public/warehouse/orders/_order.html.erb new file mode 100644 index 0000000..a35b117 --- /dev/null +++ b/app/views/public/warehouse/orders/_order.html.erb @@ -0,0 +1,15 @@ + + <%= order.created_at.strftime("%Y-%m-%d") %> + + <% if order.user_facing_title %> + Warehouse: <%= order.user_facing_title %> + <% else %> + Warehouse shipment <%= order.public_id %> + <% end %> + + <%= order.humanized_state %> + + <%= link_to "Open", public_package_path(order), + onclick: "openIframeWindow(this.href, 'Warehouse: #{order.user_facing_title || order.public_id}'); return false;", class: 'open-link' %> + + \ No newline at end of file diff --git a/app/views/public/warehouse/orders/show.html.erb b/app/views/public/warehouse/orders/show.html.erb new file mode 100644 index 0000000..2b6ffc1 --- /dev/null +++ b/app/views/public/warehouse/orders/show.html.erb @@ -0,0 +1,90 @@ +<% content_for :window_title do %> + <% if @package.user_facing_title %> + Warehouse: <%= @package.user_facing_title %> + <% else %> + Warehouse shipment <%= @package.public_id %> + <% end %> +<% end %> +<% content_for :back_office_link do %> + <%= link_to "view this package", warehouse_order_path(@package) %> in the backend? +<% end %> +
+ Status: + <%= @package.humanized_state %> +
+<% if @package.tracking_number %> +
+ Tracking: +
+ <%= @package.pretty_via %> + <%= link_to @package.tracking_number, @package.tracking_url, target: "_blank" %> + <%= copy_to_clipboard(@package.tracking_number) do %> + 📋 + <% end %> +
+ <% if @package.might_be_slow? %> +
+ Note: This carrier may have slower tracking updates +
+ <% end %> +
+<% end %> +
+ Contents: +
+
+ + <% @package.line_items.includes(:sku).each do |item| %> + + + + + <% end %> +
<%= item.sku.name %>× <%= item.quantity %>
+
+
+
+<% back_office_tool do %> +
+ Created by: <%= @package.user&.username || "who knows?" %>
+ Costs: +
+
+ + + + + + + + + + <% if @package.postage_cost %> + + + + + <% end %> + + + + +
Contents:<%= number_to_currency(@package.contents_actual_cost_to_hc) %>
Labor:<%= number_to_currency(@package.labor_cost) %>
Postage:<%= number_to_currency(@package.postage_cost) %>
Total:<%= number_to_currency(@package.total_cost) %>
+
+
+
+<% end %> +<% if @package.tags.any? %> +
+ Tags: +
+ <%= @package.tags.join(", ") %> +
+
+<% end %> +<% if current_public_user&.email == @package.recipient_email && !@package.address.us? && @package.mailed? %> +
+ <%= button_to "generate customs receipt", package_customs_receipt_path(@package), method: :get %> +
+<% end %> +<%= render partial: 'application/admin_inspector', locals: { record: @package } %> diff --git a/app/views/public_ids/index.html.erb b/app/views/public_ids/index.html.erb new file mode 100644 index 0000000..f69498f --- /dev/null +++ b/app/views/public_ids/index.html.erb @@ -0,0 +1,6 @@ +

enter one of those IDs we're always putting on things:

+<%= form_with url: lookup_public_ids_path, method: :post do |f| %> + <%= f.label :id, "ID:" %> + <%= f.text_field :id %> + <%= f.submit "go!", class: "btn success btn-small" %> +<% end %> diff --git a/app/views/pwa/manifest.json.erb b/app/views/pwa/manifest.json.erb new file mode 100644 index 0000000..c214107 --- /dev/null +++ b/app/views/pwa/manifest.json.erb @@ -0,0 +1,22 @@ +{ + "name": "Theseus", + "icons": [ + { + "src": "/icon.png", + "type": "image/png", + "sizes": "512x512" + }, + { + "src": "/icon.png", + "type": "image/png", + "sizes": "512x512", + "purpose": "maskable" + } + ], + "start_url": "/", + "display": "standalone", + "scope": "/", + "description": "Theseus.", + "theme_color": "red", + "background_color": "red" +} diff --git a/app/views/pwa/service-worker.js b/app/views/pwa/service-worker.js new file mode 100644 index 0000000..b3a13fb --- /dev/null +++ b/app/views/pwa/service-worker.js @@ -0,0 +1,26 @@ +// Add a service worker for processing Web Push notifications: +// +// self.addEventListener("push", async (event) => { +// const { title, options } = await event.data.json() +// event.waitUntil(self.registration.showNotification(title, options)) +// }) +// +// self.addEventListener("notificationclick", function(event) { +// event.notification.close() +// event.waitUntil( +// clients.matchAll({ type: "window" }).then((clientList) => { +// for (let i = 0; i < clientList.length; i++) { +// let client = clientList[i] +// let clientPath = (new URL(client.url)).pathname +// +// if (clientPath == event.notification.data.path && "focus" in client) { +// return client.focus() +// } +// } +// +// if (clients.openWindow) { +// return clients.openWindow(event.notification.data.path) +// } +// }) +// ) +// }) diff --git a/app/views/qz_trays/settings.html.erb b/app/views/qz_trays/settings.html.erb new file mode 100644 index 0000000..9e5a7b1 --- /dev/null +++ b/app/views/qz_trays/settings.html.erb @@ -0,0 +1,9 @@ +<%= content_for :head do %> + <%= vite_javascript_tag "qz" %> + <%= vite_javascript_tag "qz_settings" %> +<% end %> + +

print settings (master those rasters!)

+(thanks <%= link_to 'QZ', 'https://qz.io/', target: '_blank' %> for the certs!)

+ +
diff --git a/app/views/return_addresses/_form.html.erb b/app/views/return_addresses/_form.html.erb new file mode 100644 index 0000000..9f211ec --- /dev/null +++ b/app/views/return_addresses/_form.html.erb @@ -0,0 +1,75 @@ +<%= form_with(model: return_address, local: true) do |form| %> + <% if return_address.errors.any? %> +
+

<%= pluralize(return_address.errors.count, "error") %> prohibited this return address from being saved:

+ +
    + <% return_address.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> + +
+
+
+
+ <%= form.label :name, class: "form-label" %> + <%= form.text_field :name, class: "form-control", placeholder: "Organization or Personal Name" %> +
+ +
+ <%= form.label :line_1, "Address Line 1", class: "form-label" %> + <%= form.text_field :line_1, class: "form-control", placeholder: "Street address, P.O. box, etc." %> +
+
+ +
+ <%= form.label :line_2, "Address Line 2", class: "form-label" %> + <%= form.text_field :line_2, class: "form-control", placeholder: "Apartment, suite, unit, etc. (optional)" %> +
+ +
+
+ <%= form.label :city, class: "form-label" %> + <%= form.text_field :city, class: "form-control" %> +
+ +
+ <%= form.label :state, class: "form-label" %> + <%= form.text_field :state, class: "form-control" %> +
+ +
+ <%= form.label :postal_code, "Postal Code", class: "form-label" %> + <%= form.text_field :postal_code, class: "form-control" %> +
+
+ +
+ <%= form.label :country, class: "form-label" %> + <%= form.collection_select :country, + ReturnAddress.countries_for_select, + :first, + :last, + { include_blank: "Select a country" }, + { class: "form-control" } %> +
+ +
+ +
+ + <%= form.hidden_field :user_id, value: current_user.id %> + <%= form.hidden_field :from_letter, value: true if params[:from_letter].present? %> +
+
+ +
+ <%= form.submit class: "btn btn-primary" %> +
+<% end %> \ No newline at end of file diff --git a/app/views/return_addresses/_picker.html.erb b/app/views/return_addresses/_picker.html.erb new file mode 100644 index 0000000..c795162 --- /dev/null +++ b/app/views/return_addresses/_picker.html.erb @@ -0,0 +1,102 @@ +<%# + This partial expects the following variables: + - form: the form object + + You can call it like this: + <%= render 'return_addresses/picker', form: form %> + + +
+
+ <%= form.label :return_address_id, "Return Address", class: "form-label" %> + +
+ <%= form.collection_select :return_address_id, + ReturnAddress.shared.or(ReturnAddress.owned_by(current_user)), + :id, + :display_name, + { prompt: "Select a return address...", selected: 1 }, + { class: "form-control" } %> + + +
+
+ + +
+ + \ No newline at end of file diff --git a/app/views/return_addresses/edit.html.erb b/app/views/return_addresses/edit.html.erb new file mode 100644 index 0000000..cc003f7 --- /dev/null +++ b/app/views/return_addresses/edit.html.erb @@ -0,0 +1,9 @@ +<% content_for :title, "Editing Return Address" %> + +

Editing Return Address

+ +<%= render "form", return_address: @return_address %> + +
+ <%= link_to "Back to Return Addresses", return_addresses_path, class: "btn btn-secondary" %> +
\ No newline at end of file diff --git a/app/views/return_addresses/index.html.erb b/app/views/return_addresses/index.html.erb new file mode 100644 index 0000000..650fb24 --- /dev/null +++ b/app/views/return_addresses/index.html.erb @@ -0,0 +1,50 @@ +

Return Addresses

+
+ <%= create_button new_return_address_path, "new!" %> +
+<% if @return_addresses.any? %> +
+ <% @return_addresses.each do |address| %> +
+
+

<%= address.name %> + <% if address == current_user&.home_return_address %> + 🏠 + <% end %> + <% if address.shared %> + Shared + <% end %> + <% if address.user == current_user %> + Mine + <% end %> +

+
+
+

+ <%= address.line_1 %>
+ <% if address.line_2.present? %><%= address.line_2 %>
+ <% end %> + <%= address.city %>, <%= address.state %> <%= address.postal_code %>
+ <%= address.country %> +

+
+
+ <% if address.user == current_user || current_user.admin? %> +
+ <%= link_to "Edit", edit_return_address_path(address), class: "btn btn-small" %> + <%= button_to "delete", address, method: :delete, data: { confirm: "Are you sure you want to delete this return address?" }, class: "btn btn-small danger" %> + <% unless address == current_user&.home_return_address %> + <%= button_to "set as your default", set_as_home_return_address_path(address), method: :post, class: "btn btn-small success" %> + <% end %> +
+ <% end %> +
+
+ <% end %> +
+<% else %> +
+

No return addresses found.

+

<%= link_to "Create your first return address", new_return_address_path %>

+
+<% end %> diff --git a/app/views/return_addresses/new.html.erb b/app/views/return_addresses/new.html.erb new file mode 100644 index 0000000..1fa6b75 --- /dev/null +++ b/app/views/return_addresses/new.html.erb @@ -0,0 +1,9 @@ +<% content_for :title, "New Return Address" %> + +

New Return Address

+ +<%= render "form", return_address: @return_address %> + +
+ <%= link_to "Back to Return Addresses", return_addresses_path, class: "btn btn-secondary" %> +
\ No newline at end of file diff --git a/app/views/shared/_banner.erb b/app/views/shared/_banner.erb new file mode 100644 index 0000000..26ea984 --- /dev/null +++ b/app/views/shared/_banner.erb @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/app/views/shared/_batch_info.html.erb b/app/views/shared/_batch_info.html.erb new file mode 100644 index 0000000..5fef99c --- /dev/null +++ b/app/views/shared/_batch_info.html.erb @@ -0,0 +1,54 @@ +<%# This partial displays information about a batch for a letter or warehouse order %> +<% if record.batch.present? %> +
+
+

Part of Batch

+ <%= link_to letter_batch_path(record.batch), class: "text-sm text-blue-600 hover:text-blue-800" do %> + View Batch #<%= record.batch.id %> + <% end %> +
+ +
+
+
+ Batch Type: + <%= record.batch.type.split('::').first.titleize %> +
+
+ Status: + + <%= record.batch.aasm.current_state.to_s.titleize %> + +
+
+ Created: + <%= record.batch.created_at.strftime("%B %d, %Y") %> +
+
+ Items in Batch: + <%= record.batch.addresses.count %> +
+ + <% if record.batch.is_a?(Letter::Batch) %> +
+ Letter Dimensions: + <%= "#{record.batch.letter_width}\" × #{record.batch.letter_height}\"" %> +
+
+ Letter Weight: + <%= record.batch.letter_weight %> oz +
+ <% end %> + + <% if record.batch.is_a?(Warehouse::Batch) %> + <% if record.batch.warehouse_template.present? %> +
+ Template: + <%= record.batch.warehouse_template.name %> +
+ <% end %> + <% end %> +
+
+
+<% end %> \ No newline at end of file diff --git a/app/views/shared/_buttons.html.erb b/app/views/shared/_buttons.html.erb new file mode 100644 index 0000000..2df1361 --- /dev/null +++ b/app/views/shared/_buttons.html.erb @@ -0,0 +1,61 @@ +<%# Primary button (blue) %> +<% if defined?(button_path) && defined?(button_text) %> + <%= link_to button_path, class: "inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg" do %> + <% if defined?(button_icon) && button_icon.present? %> + <%= button_icon %> + <% end %> + <%= button_text %> + <% end %> +<% end %> + +<%# Secondary button (white/gray) %> +<% if defined?(secondary_path) && defined?(secondary_text) %> + <%= link_to secondary_path, class: "inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600 dark:hover:bg-gray-600" do %> + <% if defined?(secondary_icon) && secondary_icon.present? %> + <%= secondary_icon %> + <% end %> + <%= secondary_text %> + <% end %> +<% end %> + +<%# Danger button (red) %> +<% if defined?(danger_path) && defined?(danger_text) %> + <%= button_to danger_path, method: defined?(danger_method) ? danger_method : :delete, + class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500", + data: { confirm: defined?(danger_confirm) ? danger_confirm : "Are you sure? This action cannot be undone." } do %> + <% if defined?(danger_icon) && danger_icon.present? %> + <%= danger_icon %> + <% end %> + <%= danger_text %> + <% end %> +<% end %> + +<%# Warning button (yellow) %> +<% if defined?(warning_path) && defined?(warning_text) %> + <%= link_to warning_path, class: "inline-flex items-center px-4 py-2 bg-yellow-600 hover:bg-yellow-700 text-white font-medium rounded-lg" do %> + <% if defined?(warning_icon) && warning_icon.present? %> + <%= warning_icon %> + <% end %> + <%= warning_text %> + <% end %> +<% end %> + +<%# Back button - with default back icon %> +<% if defined?(back_path) %> + <%= link_to back_path, class: "inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600 dark:hover:bg-gray-600" do %> + + + + <%= defined?(back_text) ? back_text : "Back" %> + <% end %> +<% end %> + +<%# Edit button - with default edit icon %> +<% if defined?(edit_path) %> + <%= link_to edit_path, class: "inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600 dark:hover:bg-gray-600" do %> + + + + <%= defined?(edit_text) ? edit_text : "Edit" %> + <% end %> +<% end %> \ No newline at end of file diff --git a/app/views/shared/_flash.html.erb b/app/views/shared/_flash.html.erb new file mode 100644 index 0000000..aa28129 --- /dev/null +++ b/app/views/shared/_flash.html.erb @@ -0,0 +1,28 @@ +<% flash.each do |type, message| %> + <% if message.present? %> + <% alert_class = case type.to_sym + when :success then 'banner-success' + when :notice, :info then 'banner-info' + when :warning then 'banner-warning' + when :alert, :error, :danger then 'banner-danger' + else 'alert-info' + end %> + + <% end %> +<% end %> \ No newline at end of file diff --git a/app/views/shared/_instant_print_window.erb b/app/views/shared/_instant_print_window.erb new file mode 100644 index 0000000..015a391 --- /dev/null +++ b/app/views/shared/_instant_print_window.erb @@ -0,0 +1,10 @@ +<% content_for :head do %> + <%= vite_javascript_tag "qz" %> + <%= vite_javascript_tag "instant_print" %> +<% end %> +
+

printing but way quicker:

+ <%= content_tag("div", id: "instant_print_root", data: { url:, print_now: params[:print_now] }.compact_blank) { } %> +
+
+
\ No newline at end of file diff --git a/app/views/shared/_letter_attributes.html.erb b/app/views/shared/_letter_attributes.html.erb new file mode 100644 index 0000000..f9e7350 --- /dev/null +++ b/app/views/shared/_letter_attributes.html.erb @@ -0,0 +1,16 @@ +
+
+ Dimensions: + <%= record.is_a?(Letter) ? record.height : record.letter_height %> × <%= record.is_a?(Letter) ? record.width : record.letter_width %> in +
+
+ Weight: + <%= record.is_a?(Letter) ? record.weight : record.letter_weight %> oz +
+ <% if record.is_a?(Letter) %> +
+ Mailing Date: + <%= record.mailing_date&.strftime("%B %d, %Y") || "Not set" %> +
+ <% end %> +
\ No newline at end of file diff --git a/app/views/shared/_map_fields.html.erb b/app/views/shared/_map_fields.html.erb new file mode 100644 index 0000000..31a1fec --- /dev/null +++ b/app/views/shared/_map_fields.html.erb @@ -0,0 +1,295 @@ +<%# Shared template for mapping CSV fields to address fields %> +<% + default_mapping = { + # loops defaults + 'email' => 'email', + 'firstname' => 'first_name', + 'lastname' => 'last_name', + 'addressline1' => 'line_1', + 'addressline2' => 'line_2', + 'addresscity' => 'city', + 'addressstate' => 'state', + 'addresszipcode' => 'postal_code', + 'addresszip' => 'postal_code', + 'addresscountry' => 'country', + 'rubber_stamps' => 'rubber_stamps', + # hcb promotions + 'address (zip/postal code)' => 'postal_code', + 'address (city)' => 'city', + 'address (state/province)' => 'state', + 'address (country)' => 'country', + 'address (line 1)' => 'line_1', + 'address (line 2)' => 'line_2', + 'recipient name' => 'first_name', + 'login email' => 'email', + # unified YSWS DB + 'address (line 1)' => 'line_1', + 'address (line 2)' => 'line_2', + 'city' => 'city', + 'state / province' => 'state', + 'zip / postal code' => 'postal_code', + 'country' => 'country', + 'first name' => 'first_name', + 'last name' => 'last_name', + 'email' => 'email', + 'rubber stamps' => 'rubber_stamps', + 'zip/postal code' => 'postal_code', + 'city' => 'city', + 'state/province' => 'state', + 'country' => 'country', + 'line 1' => 'line_1', + 'line 2' => 'line_2', + 'address' => 'line_1' + } +%> +
+
+
+

Map CSV Fields

+

Select the corresponding address field for each CSV column.

+

Orange borders indicate automatically mapped fields. You can modify these if needed.

+
+ <%# List unmapped fields %> + <% + mapped_fields = @csv_headers.map { |header| default_mapping[header.downcase] }.compact + unmapped_fields = (@address_fields - ["batch_id"]) - mapped_fields + + # Find required fields that couldn't be automapped + required_unmapped = BaseBatchesController::REQUIRED_FIELDS.select do |field| + # Check if any CSV header could potentially map to this field + !@csv_headers.any? do |header| + default_mapping[header.downcase] == field + end + end + + optional_unmapped = unmapped_fields - BaseBatchesController::REQUIRED_FIELDS + + # Debug output + Rails.logger.debug "Available fields: #{@address_fields.inspect}" + %> + <% if unmapped_fields.any? %> +
+

Unmapped Fields

+ <% if required_unmapped.any? %> +

Required fields that need mapping:

+
    + <% required_unmapped.each do |field| %> +
  • <%= field.humanize %>
  • + <% end %> +
+ <% end %> + <% if optional_unmapped.any? %> +

Optional fields:

+
    + <% optional_unmapped.each do |field| %> +
  • <%= field.humanize %>
  • + <% end %> +
+ <% end %> +
+ <% end %> + <%= form_with(model: @batch, url: send("set_mapping_#{@batch.class.name.split('::').first.downcase}_batch_path", @batch), method: :post) do |f| %> +
+ <% + # Sort headers so automapped fields appear first + sorted_headers = @csv_headers.sort_by do |header| + default_mapping[header].present? ? 0 : 1 + end + %> + <% sorted_headers.each do |header| %> +
+
+
<%= header %>
+
+ <% if @csv_preview.first %> + <%= @csv_preview.first[@csv_headers.index(header)] %> + <% end %> +
+
+
+ <% + default_value = default_mapping[header.downcase] + is_automapped = default_value.present? + %> + <%= select_tag "mapping[#{header}]", + options_for_select(@address_fields - ["batch_id"], default_value), + prompt: "Select address field...", + class: "form-select", + data: { + csv_field: header, + automap_value: default_value + } %> +
+
+ <% end %> +
+ <%# Add hidden fields for required fields if they're not already mapped %> + <% + mapped_fields = @csv_headers.map { |header| default_mapping[header] }.compact + missing_required = BaseBatchesController::REQUIRED_FIELDS - mapped_fields + + if missing_required.any? + # Find a suitable CSV header for each missing required field + missing_required.each do |field| + # Try to find a suitable header based on field name + suitable_header = case field + when 'first_name' + @csv_headers.find { |h| h.downcase.include?('name') || h.downcase.include?('recipient') } + when 'state' + @csv_headers.find { |h| h.downcase.include?('state') || h.downcase.include?('province') } + when 'line_1' + @csv_headers.find { |h| h.downcase.include?('address') || h.downcase.include?('street') } + when 'city' + @csv_headers.find { |h| h.downcase.include?('city') || h.downcase.include?('town') } + when 'postal_code' + @csv_headers.find { |h| h.downcase.include?('zip') || h.downcase.include?('postal') || h.downcase.include?('code') } + when 'country' + @csv_headers.find { |h| h.downcase.include?('country') || h.downcase.include?('nation') } + end + + if suitable_header + # Add a hidden field to map this header to the required field + hidden_field_tag "mapping[#{suitable_header}]", field + end + end + end + %> +
+ <%= f.submit "Save Mapping", class: "contrast" %> + <%= link_to "Cancel", send("#{@batch.class.name.split('::').first.downcase}_batch_path", @batch), class: "secondary" %> +
+ <% end %> +
+
+ + \ No newline at end of file diff --git a/app/views/shared/_nav.html.erb b/app/views/shared/_nav.html.erb new file mode 100644 index 0000000..a4fa359 --- /dev/null +++ b/app/views/shared/_nav.html.erb @@ -0,0 +1,117 @@ + + + \ No newline at end of file diff --git a/app/views/shared/_page_header.html.erb b/app/views/shared/_page_header.html.erb new file mode 100644 index 0000000..e0c3cf8 --- /dev/null +++ b/app/views/shared/_page_header.html.erb @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/app/views/shared/_tag_picker.html.erb b/app/views/shared/_tag_picker.html.erb new file mode 100644 index 0000000..8d9b5d0 --- /dev/null +++ b/app/views/shared/_tag_picker.html.erb @@ -0,0 +1,50 @@ +<% content_for :head do %> + <%= vite_javascript_tag "taggable" %> +<% end %> +<%# Tag picker partial that can be used for any taggable model %> +<%# Usage: render 'shared/tag_picker', form: form, field_name: :tags %> +<% field_name ||= :tags %> +<% selected_tags = form.object.try(:tags) || [] %> +<% all_tags = available_tags(selected_tags) %> + +
+ <%= form.label field_name, "Tags", class: "form-label" %> + <%= form.select field_name, + all_tags, + { selected: selected_tags, include_blank: false }, + { multiple: true, + class: "form-control selectize-tags", + data: { + selectize: { + plugins: ['remove_button'], + persist: false, + create: true, + maxItems: nil + } + } + } %> +
Select from common tags or create your own
+
+ + \ No newline at end of file diff --git a/app/views/shared/_tags.html.erb b/app/views/shared/_tags.html.erb new file mode 100644 index 0000000..202094b --- /dev/null +++ b/app/views/shared/_tags.html.erb @@ -0,0 +1,9 @@ +<%# Tags display partial that can be used for any taggable model %> +<%# Usage: render 'shared/tags', tags: model.tags %> +<% if tags.present? %> +
+ <% tags.compact_blank.each do |tag| %> + <%= render 'tags/tag', tag: %> + <% end %> +
+<% end %> \ No newline at end of file diff --git a/app/views/shared/_template_picker.erb b/app/views/shared/_template_picker.erb new file mode 100644 index 0000000..4b370d2 --- /dev/null +++ b/app/views/shared/_template_picker.erb @@ -0,0 +1,161 @@ +<%# Template picker partial that supports both single and multiple selection %> +
+
+

Select Template<%= local_assigns[:multiple] ? "s" : "" %>

+ <% if local_assigns[:multiple] %> +
+ +
+ <% end %> +
+
+ <% templates = (local_assigns[:multiple] || local_assigns[:show_all]) ? SnailMail::Templates.available_templates : SnailMail::Templates.available_single_templates %> + <% templates.each do |template_name| %> + <% template_class = SnailMail::Templates.get_template_class(template_name) %> + <% preview_image = "images/template_previews/#{template_class.name.split('::').last.underscore}.png" %> +
+
+ <%= vite_image_tag preview_image, alt: "#{template_name} preview", class: "template-picker__image" %> +
+
+ <%= template_name.to_s.titleize %> +
+ <% if local_assigns[:multiple] %> +
+ <% end %> +
+ <% end %> +
+ +
+ diff --git a/app/views/shared/_user_mention.html.erb b/app/views/shared/_user_mention.html.erb new file mode 100644 index 0000000..8e0339b --- /dev/null +++ b/app/views/shared/_user_mention.html.erb @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/app/views/source_tags/_form.html.erb b/app/views/source_tags/_form.html.erb new file mode 100644 index 0000000..4865708 --- /dev/null +++ b/app/views/source_tags/_form.html.erb @@ -0,0 +1,32 @@ +<%= form_with(model: source_tag) do |form| %> + <% if source_tag.errors.any? %> +
+

<%= pluralize(source_tag.errors.count, "error") %> prohibited this source_tag from being saved:

+ +
    + <% source_tag.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> + +
+ <%= form.label :slug, style: "display: block" %> + <%= form.text_field :slug %> +
+ +
+ <%= form.label :name, style: "display: block" %> + <%= form.text_field :name %> +
+ +
+ <%= form.label :owner, style: "display: block" %> + <%= form.text_field :owner %> +
+ +
+ <%= form.submit %> +
+<% end %> diff --git a/app/views/source_tags/_source_tag.html.erb b/app/views/source_tags/_source_tag.html.erb new file mode 100644 index 0000000..e0e6a11 --- /dev/null +++ b/app/views/source_tags/_source_tag.html.erb @@ -0,0 +1,17 @@ +
+

+ Slug: + <%= source_tag.slug %> +

+ +

+ Name: + <%= source_tag.name %> +

+ +

+ Owner: + <%= source_tag.owner %> +

+ +
diff --git a/app/views/source_tags/_source_tag.json.jbuilder b/app/views/source_tags/_source_tag.json.jbuilder new file mode 100644 index 0000000..23f64fd --- /dev/null +++ b/app/views/source_tags/_source_tag.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! source_tag, :id, :slug, :name, :owner, :created_at, :updated_at +json.url source_tag_url(source_tag, format: :json) diff --git a/app/views/source_tags/edit.html.erb b/app/views/source_tags/edit.html.erb new file mode 100644 index 0000000..fcb7a04 --- /dev/null +++ b/app/views/source_tags/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing source tag" %> + +

Editing source tag

+ +<%= render "form", source_tag: @source_tag %> + +
+ +
+ <%= link_to "Show this source tag", @source_tag %> | + <%= link_to "Back to source tags", source_tags_path %> +
diff --git a/app/views/source_tags/index.html.erb b/app/views/source_tags/index.html.erb new file mode 100644 index 0000000..1927d83 --- /dev/null +++ b/app/views/source_tags/index.html.erb @@ -0,0 +1,14 @@ +<% content_for :title, "Source tags" %> + +

Source tags

+ +
+ <% @source_tags.each do |source_tag| %> + <%= render source_tag %> +

+ <%= link_to "Show this source tag", source_tag %> +

+ <% end %> +
+ +<%= link_to "New source tag", new_source_tag_path %> diff --git a/app/views/source_tags/index.json.jbuilder b/app/views/source_tags/index.json.jbuilder new file mode 100644 index 0000000..6906931 --- /dev/null +++ b/app/views/source_tags/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @source_tags, partial: "source_tags/source_tag", as: :source_tag diff --git a/app/views/source_tags/new.html.erb b/app/views/source_tags/new.html.erb new file mode 100644 index 0000000..ad73603 --- /dev/null +++ b/app/views/source_tags/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "New source tag" %> + +

New source tag

+ +<%= render "form", source_tag: @source_tag %> + +
+ +
+ <%= link_to "Back to source tags", source_tags_path %> +
diff --git a/app/views/source_tags/show.html.erb b/app/views/source_tags/show.html.erb new file mode 100644 index 0000000..acdab56 --- /dev/null +++ b/app/views/source_tags/show.html.erb @@ -0,0 +1,8 @@ +<%= render @source_tag %> + +
+ <%= link_to "Edit this source tag", edit_source_tag_path(@source_tag) %> | + <%= link_to "Back to source tags", source_tags_path %> + + <%= button_to "Destroy this source tag", @source_tag, method: :delete %> +
diff --git a/app/views/source_tags/show.json.jbuilder b/app/views/source_tags/show.json.jbuilder new file mode 100644 index 0000000..3891fdc --- /dev/null +++ b/app/views/source_tags/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "source_tags/source_tag", source_tag: @source_tag diff --git a/app/views/static_pages/index.html.erb b/app/views/static_pages/index.html.erb new file mode 100644 index 0000000..59b6a1f --- /dev/null +++ b/app/views/static_pages/index.html.erb @@ -0,0 +1,27 @@ +

Where do you want to go today?

+

I'd like to...

+
    +
  • + Go to the warehouse and... +
      +
    • <%= link_to "look at SKUs", warehouse_skus_path %>
    • +
    • <%= link_to "request a package!", new_warehouse_order_path %>
    • +
    +
  • +
  • + Stop by the post office and... +
      +
    • <%= link_to "send someone a letter!", new_letter_path %>
    • +
    • <%= link_to "send a lot of people a lot of letters?", new_letter_batch_path %>
    • +
    +
  • +
  • + <%= link_to "Visit", public_root_path %> the public-facing site... +
  • +
  • + <%= link_to "Look something up", public_ids_path %> by ID... +
  • +
  • + generate a <%= link_to "customs receipt", customs_receipts_path %>... +
  • +
\ No newline at end of file diff --git a/app/views/static_pages/login.html.erb b/app/views/static_pages/login.html.erb new file mode 100644 index 0000000..9bb81a0 --- /dev/null +++ b/app/views/static_pages/login.html.erb @@ -0,0 +1,13 @@ + + + + <%= vite_client_tag %> + <%= vite_stylesheet_tag 'login_page.scss' %> + + +<%= vite_image_tag 'images/login/treasure.png', id: "treasure" %> +<%= render 'shared/flash' %> +

welcome ashore...

+<%= link_to "log in?", slack_auth_path %> + + diff --git a/app/views/tags/_tag.html.erb b/app/views/tags/_tag.html.erb new file mode 100644 index 0000000..2fb45a7 --- /dev/null +++ b/app/views/tags/_tag.html.erb @@ -0,0 +1,5 @@ + + <%= link_to tag_stats_path(tag) do %> + <%= tag %> + <% end %> + diff --git a/app/views/tags/index.html.erb b/app/views/tags/index.html.erb new file mode 100644 index 0000000..a5cd6b8 --- /dev/null +++ b/app/views/tags/index.html.erb @@ -0,0 +1,15 @@ +<% content_for :title, "Tags" %> + +

Tags

+ +<%= button_to "↻", refresh_tags_path %> +

Common tags:

+<% @common_tags.each do |tag| %> + <%= render 'tag', tag: tag.tag %> +<% end %> +

Other tags:

+<% @tags.each do |tag| %> + <%= render 'tag', tag: %> +<% end %> \ No newline at end of file diff --git a/app/views/tags/show.html.erb b/app/views/tags/show.html.erb new file mode 100644 index 0000000..82203e4 --- /dev/null +++ b/app/views/tags/show.html.erb @@ -0,0 +1,81 @@ +

Tag: "<%= @tag %>"

+
+ currently showing: + + + + + + + + + +
+
+

+ <%= pluralize(@warehouse_order_count, 'warehouse order') %>, <%= pluralize(@letter_count, 'letter') %> +

+

+ Letter postage spent: + <%= number_to_currency(@letter_postage_cost) %> +

+
+

+ Warehouse costs:
+
+ Postage: <%= number_to_currency(@warehouse_order_postage_cost) %>
+ Labor: <%= number_to_currency(@warehouse_order_labor_cost) %>
+ Contents: <%= number_to_currency(@warehouse_order_contents_cost) %>
+ Total: <%= number_to_currency(@warehouse_order_total_cost) %> +

+
+ <%= link_to "< back to tags", tags_path %> +
+ diff --git a/app/views/tasks/badge.html.erb b/app/views/tasks/badge.html.erb new file mode 100644 index 0000000..de227ee --- /dev/null +++ b/app/views/tasks/badge.html.erb @@ -0,0 +1,24 @@ + + + + <%= vite_client_tag %> + <%= vite_stylesheet_tag 'app_style.scss' %> + + + <% if @tasks.any? %> + <%= link_to tasks_path, target: "_top" do%> +
+ <%= @tasks.count %><% if @tasks.count > 50 %>?<% end %> +
+ <% end %> + <% end %> + + diff --git a/app/views/tasks/show.html.erb b/app/views/tasks/show.html.erb new file mode 100644 index 0000000..559be3c --- /dev/null +++ b/app/views/tasks/show.html.erb @@ -0,0 +1,18 @@ +

your tasks: + <%= button_to "↻", refresh_tasks_path, method: :post, class: "btn btn-tiny success" %> +

+
+
    + <% @tasks.group_by { |task| task[:type] }.each do |type, tasks| %> +
  • <%= type %>:
  • +
      + <% tasks.each do |task| %> +
    • <%= task[:name] %> + <% if task[:subtitle] %> + (<%= task[:subtitle] %>) + <% end %> + <%= link_to "(go!)", task[:link] %>
    • + <% end %> +
    + <% end %> +
diff --git a/app/views/usps/indicia/_form.html.erb b/app/views/usps/indicia/_form.html.erb new file mode 100644 index 0000000..b646403 --- /dev/null +++ b/app/views/usps/indicia/_form.html.erb @@ -0,0 +1,67 @@ +<%= form_with(model: usps_indicium) do |form| %> + <% if usps_indicium.errors.any? %> +
+

<%= pluralize(usps_indicium.errors.count, "error") %> prohibited this usps_indicium from being saved:

+ +
    + <% usps_indicium.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> + +
+ <%= form.label :processing_category, style: "display: block" %> + <%= form.number_field :processing_category %> +
+ +
+ <%= form.label :postage_weight, style: "display: block" %> + <%= form.text_field :postage_weight %> +
+ +
+ <%= form.label :postage_length, style: "display: block" %> + <%= form.text_field :postage_length %> +
+ +
+ <%= form.label :postage_height, style: "display: block" %> + <%= form.text_field :postage_height %> +
+ +
+ <%= form.label :postage_thickness, style: "display: block" %> + <%= form.text_field :postage_thickness %> +
+ +
+ <%= form.label :nonmachinable, style: "display: block" %> + <%= form.checkbox :nonmachinable %> +
+ +
+ <%= form.label :usps_sku, style: "display: block" %> + <%= form.text_field :usps_sku %> +
+ +
+ <%= form.label :raw_usps_response, style: "display: block" %> + <%= form.textarea :raw_usps_response %> +
+ +
+ <%= form.label :postage, style: "display: block" %> + <%= form.text_field :postage %> +
+ +
+ <%= form.label :mailing_date, style: "display: block" %> + <%= form.date_field :mailing_date %> +
+ +
+ <%= form.submit %> +
+<% end %> diff --git a/app/views/usps/indicia/_indicium.html.erb b/app/views/usps/indicia/_indicium.html.erb new file mode 100644 index 0000000..c10d0a3 --- /dev/null +++ b/app/views/usps/indicia/_indicium.html.erb @@ -0,0 +1,52 @@ +
+

+ Processing category: + <%= indicium.processing_category %> +

+ +

+ Postage weight: + <%= indicium.postage_weight %> +

+ +

+ Postage length: + <%= indicium.postage_length %> +

+ +

+ Postage height: + <%= indicium.postage_height %> +

+ +

+ Postage thickness: + <%= indicium.postage_thickness %> +

+ +

+ Nonmachinable: + <%= indicium.nonmachinable %> +

+ +

+ USPS sku: + <%= indicium.usps_sku %> +

+ +

+ Raw USPS response: + <%= indicium.raw_usps_response %> +

+ +

+ Postage: + <%= indicium.postage %> +

+ +

+ Mailing date: + <%= indicium.mailing_date %> +

+ +
diff --git a/app/views/usps/indicia/edit.html.erb b/app/views/usps/indicia/edit.html.erb new file mode 100644 index 0000000..bb09f04 --- /dev/null +++ b/app/views/usps/indicia/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing indicium" %> + +

Editing indicium

+ +<%= render "form", usps_indicium: @usps_indicium %> + +
+ +
+ <%= link_to "Show this indicium", @usps_indicium %> | + <%= link_to "Back to indicia", usps_indicia_path %> +
diff --git a/app/views/usps/indicia/index.html.erb b/app/views/usps/indicia/index.html.erb new file mode 100644 index 0000000..8f6144b --- /dev/null +++ b/app/views/usps/indicia/index.html.erb @@ -0,0 +1,14 @@ +<% content_for :title, "Indicia" %> + +

Indicia

+ +
+ <% @usps_indicia.each do |usps_indicium| %> + <%= render usps_indicium %> +

+ <%= link_to "Show this indicium", usps_indicium %> +

+ <% end %> +
+ +<%= link_to "New indicium", new_usps_indicium_path %> diff --git a/app/views/usps/indicia/new.html.erb b/app/views/usps/indicia/new.html.erb new file mode 100644 index 0000000..862fca0 --- /dev/null +++ b/app/views/usps/indicia/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "New indicium" %> + +

New indicium

+ +<%= render "form", usps_indicium: @usps_indicium %> + +
+ +
+ <%= link_to "Back to indicia", usps_indicia_path %> +
diff --git a/app/views/usps/indicia/show.html.erb b/app/views/usps/indicia/show.html.erb new file mode 100644 index 0000000..7b98f95 --- /dev/null +++ b/app/views/usps/indicia/show.html.erb @@ -0,0 +1,9 @@ +<%= render @usps_indicium %> +<%= render partial: "admin_inspector", locals: { record: @usps_indicium } %> + +
+ <%= link_to "Edit this indicium", edit_usps_indicium_path(@usps_indicium) %> | + <%= link_to "Back to indicia", usps_indicia_path %> + + <%= button_to "Destroy this indicium", @usps_indicium, method: :delete %> +
diff --git a/app/views/usps/mailer_ids/_form.html.erb b/app/views/usps/mailer_ids/_form.html.erb new file mode 100644 index 0000000..3fb2ece --- /dev/null +++ b/app/views/usps/mailer_ids/_form.html.erb @@ -0,0 +1,33 @@ +<%= form_with(model: usps_mailer_id) do |form| %> + <% if usps_mailer_id.errors.any? %> +
+

<%= pluralize(usps_mailer_id.errors.count, "error") %> prohibited this usps_mailer_id from being saved:

+ +
    + <% usps_mailer_id.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> + + +
+ <%= form.label :name, style: "display: block" %> + <%= form.text_field :name %> +
+ +
+ <%= form.label :crid, style: "display: block" %> + <%= form.text_field :crid %> +
+ +
+ <%= form.label :mid, style: "display: block" %> + <%= form.text_field :mid %> +
+ +
+ <%= form.submit %> +
+<% end %> diff --git a/app/views/usps/mailer_ids/_mailer_id.html.erb b/app/views/usps/mailer_ids/_mailer_id.html.erb new file mode 100644 index 0000000..3b5ec41 --- /dev/null +++ b/app/views/usps/mailer_ids/_mailer_id.html.erb @@ -0,0 +1,18 @@ +
+ +

+ Name: + <%= mailer_id.name %> +

+ +

+ Crid: + <%= mailer_id.crid %> +

+ +

+ Mid: + <%= mailer_id.mid %> +

+ +
diff --git a/app/views/usps/mailer_ids/edit.html.erb b/app/views/usps/mailer_ids/edit.html.erb new file mode 100644 index 0000000..21e0ae9 --- /dev/null +++ b/app/views/usps/mailer_ids/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing mailer" %> + +

Editing mailer

+ +<%= render "form", usps_mailer_id: @usps_mailer_id %> + +
+ +
+ <%= link_to "back to MID", @usps_mailer_id %> | + <%= link_to "back to all MIDs", usps_mailer_ids_path %> +
diff --git a/app/views/usps/mailer_ids/index.html.erb b/app/views/usps/mailer_ids/index.html.erb new file mode 100644 index 0000000..35a8a94 --- /dev/null +++ b/app/views/usps/mailer_ids/index.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "Mailers" %> + +

CRID/Mailer ID Pairs:

+ +
+ <% @usps_mailer_ids.each do |mid| %> + <%= link_to mid.name, mid %> (<%= mid.crid %>/<%= mid.mid %>)
+ <% end %> +
+
+<%= link_to "enter a new MID?", new_usps_mailer_id_path %> diff --git a/app/views/usps/mailer_ids/new.html.erb b/app/views/usps/mailer_ids/new.html.erb new file mode 100644 index 0000000..b3aa483 --- /dev/null +++ b/app/views/usps/mailer_ids/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "New mailer" %> + +

New mailer

+ +<%= render "form", usps_mailer_id: @usps_mailer_id %> + +
+ +
+ <%= link_to "back to MIDs", usps_mailer_ids_path %> +
diff --git a/app/views/usps/mailer_ids/show.html.erb b/app/views/usps/mailer_ids/show.html.erb new file mode 100644 index 0000000..20afc56 --- /dev/null +++ b/app/views/usps/mailer_ids/show.html.erb @@ -0,0 +1,9 @@ +<%= render @usps_mailer_id %> +<%= render partial: "admin_inspector", locals: { record: @usps_mailer_id } %> + +
+ <%= link_to "edit this MID", edit_usps_mailer_id_path(@usps_mailer_id) %> | + <%= link_to "back to MIDs", usps_mailer_ids_path %> + + <%= button_to "destroy this MID", @usps_mailer_id, method: :delete %> +
diff --git a/app/views/usps/payment_account_mailer/get_your_eps_racks_up.text.erb b/app/views/usps/payment_account_mailer/get_your_eps_racks_up.text.erb new file mode 100644 index 0000000..27cb307 --- /dev/null +++ b/app/views/usps/payment_account_mailer/get_your_eps_racks_up.text.erb @@ -0,0 +1,14 @@ +what up nora + +<%= "this".pluralize(@count) %> <%= "account".pluralize(@count) %> <%= "is".pluralize(@count) %> under <%= number_to_currency(USPS::PaymentAccount::PocketWatchJob::THRESHOLD) %>: +
    + <% @accounts.each do |account| %> +
  • + <%= link_to account %> +
  • + <% end %> +
+ +maybe throw some cash in there idk + +k cya \ No newline at end of file diff --git a/app/views/usps/payment_accounts/_form.html.erb b/app/views/usps/payment_accounts/_form.html.erb new file mode 100644 index 0000000..f9ca121 --- /dev/null +++ b/app/views/usps/payment_accounts/_form.html.erb @@ -0,0 +1,52 @@ +<%= form_with(model: usps_payment_account) do |form| %> + <% if usps_payment_account.errors.any? %> +
+

<%= pluralize(usps_payment_account.errors.count, "error") %> prohibited this usps_payment_account from being saved:

+ +
    + <% usps_payment_account.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> +
+ <%= form.label :name, "Display name:" %> + <%= form.text_field :name %> +
+
+ <%= form.label :usps_mailer_id_id, "CRID/MID:" %> + <%= form.collection_select(:usps_mailer_id_id, USPS::MailerId.all, :id, :display_name, prompt: 'select one...', required: true) %> +
+
+ <%= form.label :account_type, "Account type:" %> + <%= form.select :account_type, USPS::PaymentAccount.account_types.keys, { selected: @usps_payment_account.account_type }, { 'x-model' => 'account_type' } %> + + +
+ +
+ <%= form.label :manifest_mid, "Manifest MID:" %> + <%= form.text_field :manifest_mid %> +
+ +
+ <%= form.submit %> +
+<% end %> diff --git a/app/views/usps/payment_accounts/_payment_account.html.erb b/app/views/usps/payment_accounts/_payment_account.html.erb new file mode 100644 index 0000000..9eaa79a --- /dev/null +++ b/app/views/usps/payment_accounts/_payment_account.html.erb @@ -0,0 +1,3 @@ +
+ <%= payment_account.display_name %> +
diff --git a/app/views/usps/payment_accounts/edit.html.erb b/app/views/usps/payment_accounts/edit.html.erb new file mode 100644 index 0000000..57d2e5b --- /dev/null +++ b/app/views/usps/payment_accounts/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing payment account" %> + +

Editing payment account

+ +<%= render "form", usps_payment_account: @usps_payment_account %> + +
+ +
+ <%= link_to "Show this payment account", @usps_payment_account %> | + <%= link_to "Back to payment accounts", usps_payment_accounts_path %> +
diff --git a/app/views/usps/payment_accounts/index.html.erb b/app/views/usps/payment_accounts/index.html.erb new file mode 100644 index 0000000..fcdb818 --- /dev/null +++ b/app/views/usps/payment_accounts/index.html.erb @@ -0,0 +1,14 @@ +<% content_for :title, "Payment accounts" %> + +

Payment accounts

+ +
+ <% @usps_payment_accounts.each do |usps_payment_account| %> + <%= render usps_payment_account %> +

+ <%= link_to "Show this payment account", usps_payment_account %> +

+ <% end %> +
+ +<%= link_to "New payment account", new_usps_payment_account_path %> diff --git a/app/views/usps/payment_accounts/new.html.erb b/app/views/usps/payment_accounts/new.html.erb new file mode 100644 index 0000000..8a2a52c --- /dev/null +++ b/app/views/usps/payment_accounts/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "New payment account" %> + +

New payment account

+ +<%= render "form", usps_payment_account: @usps_payment_account %> + +
+ +
+ <%= link_to "Back to payment accounts", usps_payment_accounts_path %> +
diff --git a/app/views/usps/payment_accounts/show.html.erb b/app/views/usps/payment_accounts/show.html.erb new file mode 100644 index 0000000..6b76099 --- /dev/null +++ b/app/views/usps/payment_accounts/show.html.erb @@ -0,0 +1,9 @@ +<%= render @usps_payment_account %> +<%= render partial: "admin_inspector", locals: { record: @usps_payment_account } %> + +
+ <%= link_to "Edit this payment account", edit_usps_payment_account_path(@usps_payment_account) %> | + <%= link_to "Back to payment accounts", usps_payment_accounts_path %> + + <%= button_to "Destroy this payment account", @usps_payment_account, method: :delete %> +
diff --git a/app/views/warehouse/batches/_batch.html.erb b/app/views/warehouse/batches/_batch.html.erb new file mode 100644 index 0000000..ef26fbb --- /dev/null +++ b/app/views/warehouse/batches/_batch.html.erb @@ -0,0 +1,69 @@ +
+
+
+
+

Batch Details

+
+ Created: + <%= @batch.created_at.strftime("%B %d, %Y") %> +
+
+ Status: + <%= batch_status_badge(@batch.aasm.current_state) %> +
+
+ Addresses: + <%= @batch.addresses.count %> +
+ <% if @batch.warehouse_user_facing_title.present? %> +
+ Title: + <%= @batch.warehouse_user_facing_title %> +
+ <% end %> +
+ +
+

Warehouse Specifications

+
+ Template: + <%= @batch.warehouse_template&.name %> +
+
+
+ + <%= render 'shared/tags', tags: @batch.tags %> +
+
+ +
+
+

Actions

+
+
+ <% case @batch.aasm_state %> + <% when "awaiting_field_mapping" %> + <%= warning_link_to "Map Fields", map_fields_warehouse_batch_path(@batch), class: "w-full mb-2" do %> + + + + Map Fields + <% end %> + <% when "fields_mapped" %> + <%= success_link_to "Process Batch", process_confirm_warehouse_batch_path(@batch), class: "w-full" do %> + <%= check_icon %>Process Batch + <% end %> + <% end %> +
+
+ +<% if @batch.warehouse_template.present? %> +
+
+

Template Preview

+
+
+ <%= render partial: "warehouse/templates/template", locals: { template: @batch.warehouse_template } %> +
+
+<% end %> \ No newline at end of file diff --git a/app/views/warehouse/batches/_form.html.erb b/app/views/warehouse/batches/_form.html.erb new file mode 100644 index 0000000..e8e1190 --- /dev/null +++ b/app/views/warehouse/batches/_form.html.erb @@ -0,0 +1,39 @@ +<%= form_with(model: batch, url: warehouse_batches_path) do |form| %> + <% if batch.errors.any? %> +
+

<%= pluralize(batch.errors.count, "error") %> prohibited this batch from being saved:

+ +
    + <% batch.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> + +
+ <%= form.label :warehouse_template_id, "Template" %> + <%= form.collection_select :warehouse_template_id, + @allowed_templates, + :id, + :name, + { selected: @allowed_templates.first&.id } %> +
+ +
+ <%= form.label :warehouse_user_facing_title, "Title" %> + <%= form.text_field :warehouse_user_facing_title %> +
+ +
+ <%= form.label :csv, "CSV File" %> + <%= form.file_field :csv, accept: ".csv" %> +
+ + <%= render 'shared/tag_picker', form: form, field_name: :tags %> + +
+ <%= form.submit "Create Batch" %> + <%= link_to "Back to batches", warehouse_batches_path %> +
+<% end %> \ No newline at end of file diff --git a/app/views/warehouse/batches/edit.html.erb b/app/views/warehouse/batches/edit.html.erb new file mode 100644 index 0000000..b7b1b83 --- /dev/null +++ b/app/views/warehouse/batches/edit.html.erb @@ -0,0 +1,63 @@ +<% content_for :title, "Edit Warehouse Batch ##{@batch.id}" %> + +
+ + + <%= form_with(model: [:warehouse, @batch], url: warehouse_batch_path(@batch), method: :patch) do |form| %> + <% if @batch.errors.any? %> +
+

<%= pluralize(@batch.errors.count, "error") %> prohibited this batch from being saved:

+
    + <% @batch.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> + +
+
+

Always Editable

+
+
+
+ <%= form.label :warehouse_user_facing_title, "Title", class: "form-label" %> + <%= form.text_field :warehouse_user_facing_title, class: "form-control" %> +
+ + <%= render 'shared/tag_picker', form: form, field_name: :tags %> +
+
+ + <% if @batch.may_mark_processed? %> +
+
+

Editable Before Processing

+
+
+
+ <%= form.label :warehouse_template_id, "Template", class: "form-label" %> + <%= form.collection_select :warehouse_template_id, + Warehouse::Template.all, + :id, + :name, + { selected: @batch.warehouse_template_id }, + class: "form-select" %> +
+
+
+ <% end %> + +
+ <%= form.submit "Update Batch", class: "btn btn-primary" %> + <%= link_to "Cancel", warehouse_batch_path(@batch), class: "btn btn-secondary" %> +
+ <% end %> +
\ No newline at end of file diff --git a/app/views/warehouse/batches/index.html.erb b/app/views/warehouse/batches/index.html.erb new file mode 100644 index 0000000..5e057b7 --- /dev/null +++ b/app/views/warehouse/batches/index.html.erb @@ -0,0 +1,51 @@ +<% content_for :title, "Warehouse Batches" %> +
+ +
+
+ <% if @batches.any? %> +
+ + + + + + + + + + + + + <% @batches.each do |batch| %> + + + + + + + + + <% end %> + +
IDCreatedAddressesStatusTagsActions
<%= link_to "##{batch.id}", warehouse_batch_path(batch) %><%= batch.created_at.strftime("%B %d, %Y") %><%= batch.addresses.count %><%= batch_status_badge(batch.aasm.current_state) %><%= render 'shared/tags', tags: batch.tags %> +
+ <%= link_to "View", warehouse_batch_path(batch), class: "btn btn-sm btn-outline-primary" %> + <%= link_to "Edit", edit_warehouse_batch_path(batch), class: "btn btn-sm btn-outline-secondary" %> +
+
+
+ <% else %> +

No warehouse batches found.

+ <% end %> +
+
+
+ \ No newline at end of file diff --git a/app/views/warehouse/batches/map_fields.html.erb b/app/views/warehouse/batches/map_fields.html.erb new file mode 100644 index 0000000..3690e6e --- /dev/null +++ b/app/views/warehouse/batches/map_fields.html.erb @@ -0,0 +1,2 @@ +<% content_for :title, "Map Fields - Warehouse Batch ##{@batch.id}" %> +<%= render 'shared/map_fields' %> \ No newline at end of file diff --git a/app/views/warehouse/batches/new.html.erb b/app/views/warehouse/batches/new.html.erb new file mode 100644 index 0000000..4c88045 --- /dev/null +++ b/app/views/warehouse/batches/new.html.erb @@ -0,0 +1,14 @@ +<% content_for :title, "New Warehouse Batch" %> + +
+ + + <%= render "form", batch: @batch %> +
\ No newline at end of file diff --git a/app/views/warehouse/batches/process_warehouse.html.erb b/app/views/warehouse/batches/process_warehouse.html.erb new file mode 100644 index 0000000..6f4aa74 --- /dev/null +++ b/app/views/warehouse/batches/process_warehouse.html.erb @@ -0,0 +1,62 @@ +<% content_for :title, "Process Warehouse Batch ##{@batch.id}" %> + +
+ + +
+
+

Batch Summary

+
+
+
+ This will create <%= @batch.addresses.count %> warehouse orders, containing: +
+ +
+
    + <% @batch.warehouse_template.line_items.each do |line_item| %> +
  • + <%= line_item.quantity %>x <%= line_item.sku.name %> +
  • + <% end %> +
+
+ +
+
+ Contents Cost: + <%= number_to_currency @batch.contents_cost %> +
+
+ Warehouse Labor: + <%= number_to_currency @batch.labor_cost %> +
+
+ Postage Cost: + (to be determined) +
+
+ Estimated Total: + <%= number_to_currency @batch.total_cost %> +
+
+
+
+ +
+
+ <%= button_to process_warehouse_batch_path(@batch), + method: :post, + class: "btn btn-primary w-full" do %> + <%= check_icon %>Process Batch + <% end %> +
+
+
\ No newline at end of file diff --git a/app/views/warehouse/batches/show.html.erb b/app/views/warehouse/batches/show.html.erb new file mode 100644 index 0000000..610f59f --- /dev/null +++ b/app/views/warehouse/batches/show.html.erb @@ -0,0 +1,29 @@ +<% content_for :title, "Warehouse Batch ##{@batch.id} - #{@batch.addresses.count} addresses" %> +
+ + <%= render @batch %> + <% if @batch.orders.any? %> +
+
+ +

Orders (<%= @batch.orders.count %>)

+
+
+
+ <%= render partial: 'warehouse/orders/orders_collection', locals: { orders: @batch.orders } %> +
+
+
+ <% end %> + <%= render partial: 'admin_inspector', locals: { record: @batch } %> +
\ No newline at end of file diff --git a/app/views/warehouse/line_items/_line_item.html.erb b/app/views/warehouse/line_items/_line_item.html.erb new file mode 100644 index 0000000..2d7fbe9 --- /dev/null +++ b/app/views/warehouse/line_items/_line_item.html.erb @@ -0,0 +1,4 @@ +
+ <%= link_to line_item.sku.name, line_item.sku, target: '_blank' %> + <%= render partial: "admin_inspector", locals: {record: line_item} if params[:inspect_line_items] %> +
diff --git a/app/views/warehouse/order_mailer/order_created.text.erb b/app/views/warehouse/order_mailer/order_created.text.erb new file mode 100644 index 0000000..181d459 --- /dev/null +++ b/app/views/warehouse/order_mailer/order_created.text.erb @@ -0,0 +1,9 @@ +<%= { + transactionalId: "cm8jh9gzf0nm6wek854cgi7y6", + email: @recipient, + dataVariables: { + user_facing_title: @order.user_facing_title, + address: @order.address.us_format, + hc_id: @order.hc_id, + } + }.to_json %> \ No newline at end of file diff --git a/app/views/warehouse/order_mailer/order_shipped.text.erb b/app/views/warehouse/order_mailer/order_shipped.text.erb new file mode 100644 index 0000000..49a6ddc --- /dev/null +++ b/app/views/warehouse/order_mailer/order_shipped.text.erb @@ -0,0 +1,20 @@ +<%= { + transactionalId: "cm8jigjb90pnkol7fy24kbv64", + email: @recipient, + dataVariables: { + user_facing_title: @order.user_facing_title, + address: @order.address.us_format, + hc_id: @order.hc_id, + tracking_here_if_tracking: if @order.tracking_number.present? + "tracking number: #{link_to @order.tracking_number, @order.tracking_url}" + else + "" + end, + usps_disclaimer_if_usps: if @order.might_be_slow? + "as a small nonprofit, we send most packages by USPS First Class Mail to save on postage. estimated delivery time:
  • within USA: 3-7 days
  • outside USA: 4-6 weeks
" + else + "" + end, + via: @order.pretty_via + } + }.to_json %> \ No newline at end of file diff --git a/app/views/warehouse/orders/_form.html.slim b/app/views/warehouse/orders/_form.html.slim new file mode 100644 index 0000000..54ca919 --- /dev/null +++ b/app/views/warehouse/orders/_form.html.slim @@ -0,0 +1,40 @@ +- content_for :head + = vite_javascript_tag 'cocoon' + += form_with(model: warehouse_order) do |form| + - if warehouse_order.errors.any? + = render layout: 'shared/banner', locals: {color: 'alert'} do + b hey, slight issue: + ul + - warehouse_order.errors.each do |error| + li= error.full_message + div + = form.label :user_facing_title, "short title (shown to recipient):" + = form.text_field :user_facing_title, class: 'form-control' + + div + = form.label :recipient_email, "recipient email:" + = form.email_field :recipient_email, class: 'form-control' + + b address: + = render 'addresses/nested_form', form: form + .py-3 + = form.label :internal_notes, "internal notes:" + .display-block + = form.text_area :internal_notes, class: 'form-control' + + = form.label :notify_on_dispatch, 'send the recipient a "sent to warehouse" email? ' + = form.check_box :notify_on_dispatch, class: 'form-control' + br + small + | (they'll get an email when it ships regardless) + h4 contents: + #line_items + = form.fields_for :line_items do |line_item| + = render 'line_item_fields', f: line_item + + .links + = link_to_add_association 'add item', form, :line_items + = render 'shared/tag_picker', form: form, field_name: :tags + .pt-3 + = form.submit "save", class: 'form-control' diff --git a/app/views/warehouse/orders/_line_items.html.erb b/app/views/warehouse/orders/_line_items.html.erb new file mode 100644 index 0000000..c4b974c --- /dev/null +++ b/app/views/warehouse/orders/_line_items.html.erb @@ -0,0 +1,33 @@ + + + + + + + + <% order.line_items.each do |li| %> + + + + + + + <% end %> + + + + + +
SKUQuantityCost to HCDeclared value
+ <%= link_to li.sku.name, li.sku, target: '_blank' %> + + <%= li.quantity %> + + <%= number_to_currency li.sku.actual_cost_to_hc %> + + <%= number_to_currency li.sku.declared_unit_cost %> +
Total: + <%= number_to_currency order.contents_actual_cost_to_hc %> + + <%= number_to_currency order.contents_declared_unit_cost %> +
\ No newline at end of file diff --git a/app/views/warehouse/orders/_order.html.erb b/app/views/warehouse/orders/_order.html.erb new file mode 100644 index 0000000..68c9e0f --- /dev/null +++ b/app/views/warehouse/orders/_order.html.erb @@ -0,0 +1,42 @@ +
+
+

+ <%= link_to order.hc_id, order %> – <%= order.user_facing_title %> + <%= render 'status_badge', order: order %> +

+
+ +
+ <%= render 'shared/tags', tags: order.tags %> + +
+
+

Order Information

+
+

Recipient: <%= order.address.name_line %>

+

Email: <%= order.recipient_email %>

+
+
+ +
+

Cost Information

+
+

Labor Cost: <%= number_to_currency(order.labor_cost) %>

+

Contents Cost: <%= number_to_currency(order.contents_actual_cost_to_hc) %> +

+

+ Postage Cost: <%= order.postage_cost.present? ? number_to_currency(order.postage_cost) : "$?.??" %> +

+
+
+
+ +
+

Contents

+
+ <%= render 'warehouse/orders/line_items', order: order %> +
+
+
+
+ diff --git a/app/views/warehouse/orders/_orders_collection.html.erb b/app/views/warehouse/orders/_orders_collection.html.erb new file mode 100644 index 0000000..e3a92b2 --- /dev/null +++ b/app/views/warehouse/orders/_orders_collection.html.erb @@ -0,0 +1,22 @@ +<% orders.each do |order| %> +
+
+

+ <%= link_to order.hc_id, order %> – <%= order.user_facing_title %> + <%= render 'warehouse/orders/status_badge', order: order %> +

+
+
+
+
+ Recipient: + <%= order.address.name_line %> (<%= order.recipient_email %>) +
+
+ Tags: + <%= render 'shared/tags', tags: order.tags %> +
+
+
+
+<% end %> \ No newline at end of file diff --git a/app/views/warehouse/orders/_status_badge.html.erb b/app/views/warehouse/orders/_status_badge.html.erb new file mode 100644 index 0000000..bed28b4 --- /dev/null +++ b/app/views/warehouse/orders/_status_badge.html.erb @@ -0,0 +1,25 @@ + + + + + +<% + color, text = case order.aasm.current_state + when :draft + %w[bg-muted draft] + when :dispatched + ["info", "sent to warehouse"] + when :mailed + %w[success mailed!] + when :errored + %w[error errored?] + when :canceled + %w[warning canceled] + else + ["purple", "this shouldn't happen?!"] + end +%> + + + <%= text %> + \ No newline at end of file diff --git a/app/views/warehouse/orders/cancel.html.erb b/app/views/warehouse/orders/cancel.html.erb new file mode 100644 index 0000000..68eb6c2 --- /dev/null +++ b/app/views/warehouse/orders/cancel.html.erb @@ -0,0 +1,9 @@ +

Cancel order <%= @warehouse_order.hc_id %>:

+Please try to avoid needing to do this.
+If fulfillment of this order is already underway at the warehouse, this will error out and you'll need to contact them directly. +

+<%= form_with method: :post do |form| %> + <%= form.label :cancellation_reason, "Reason for cancellation (visible to AGH crew): " %> + <%= form.text_field :cancellation_reason %> + <%= form.submit "do it!" %> +<% end %> \ No newline at end of file diff --git a/app/views/warehouse/orders/edit.html.erb b/app/views/warehouse/orders/edit.html.erb new file mode 100644 index 0000000..74ddb86 --- /dev/null +++ b/app/views/warehouse/orders/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing order" %> + +

Editing order

+ +<%= render "form", warehouse_order: @warehouse_order %> + +
+ +
+ <%= link_to "Show this order", @warehouse_order %> | + <%= link_to "Back to orders", warehouse_orders_path %> +
diff --git a/app/views/warehouse/orders/index.html.erb b/app/views/warehouse/orders/index.html.erb new file mode 100644 index 0000000..ef111ab --- /dev/null +++ b/app/views/warehouse/orders/index.html.erb @@ -0,0 +1,41 @@ +<% content_for :title, "Orders" %> + +
    +
  • + <%= link_to "Unbatched Orders", warehouse_orders_path, class: "tabs__link #{params[:view] != 'batched' ? 'tabs__link--active' : ''}" %> +
  • +
  • + <%= link_to "Batched Orders", warehouse_orders_path(view: 'batched'), class: "tabs__link #{params[:view] == 'batched' ? 'tabs__link--active' : ''}" %> +
  • +
+
+
+
+

<%= params[:view] == 'batched' ? 'Batched' : 'Unbatched' %> Orders

+
+
+ <% if @warehouse_orders.any? %> + <%= render partial: 'warehouse/orders/orders_collection', locals: { orders: @warehouse_orders } %> + <% else %> +
+

No <%= params[:view] == 'batched' ? 'batched' : 'unbatched' %> orders found.

+
+ <% end %> +
+
+
+<% if params[:view] != 'batched' %> +
+ <%= paginate @warehouse_orders %> +
+<% end %> \ No newline at end of file diff --git a/app/views/warehouse/orders/new.html.erb b/app/views/warehouse/orders/new.html.erb new file mode 100644 index 0000000..f5bdefd --- /dev/null +++ b/app/views/warehouse/orders/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "New order" %> + +

New order

+ +<%= render "form", warehouse_order: @warehouse_order %> + +
+ +
+ <%= link_to "Back to orders", warehouse_orders_path %> +
diff --git a/app/views/warehouse/orders/show.html.erb b/app/views/warehouse/orders/show.html.erb new file mode 100644 index 0000000..7425a6e --- /dev/null +++ b/app/views/warehouse/orders/show.html.erb @@ -0,0 +1,74 @@ +
+

+ Order <%= @warehouse_order.hc_id %>: +

+ <%= render 'status_badge', order: @warehouse_order %> + <%= link_to "Show on public page", public_package_path(@warehouse_order), target: "_blank" %> +
+ +<%= render 'shared/tags', tags: @warehouse_order.tags %> + +
+ title: <%= @warehouse_order.user_facing_title %>
+ recipient email: + <%= link_to @warehouse_order.recipient_email, "mailto:#{@warehouse_order.recipient_email}" %>
+ recipient:
+ <%= render 'addresses/address', address: @warehouse_order.address %> +
+ +<% if @warehouse_order.batch.present? %> + <%= render 'shared/batch_info', record: @warehouse_order %> +<% end %> + +contents: +<%= render 'warehouse/orders/line_items', order: @warehouse_order %> + +
+ labor cost: <%= number_to_currency(@warehouse_order.labor_cost) %> +
+ +
+ actual contents cost: <%= number_to_currency(@warehouse_order.contents_actual_cost_to_hc) %> +
+ +
+ postage cost: + <% if @warehouse_order.postage_cost.present? %> + <%= number_to_currency(@warehouse_order.postage_cost) %> + <% else %> + $?.?? + <% end %> +
+ +
+ all in<%= " (excluding postage)" unless @warehouse_order.postage_cost.present? %>: + <%= number_to_currency((@warehouse_order.contents_actual_cost_to_hc || 0) + + (@warehouse_order.labor_cost || 0) + + (@warehouse_order.postage_cost || 0)) %> +
+ +
+ Internal Notes:
+ <%= @warehouse_order.internal_notes %> +
+ +
+ +<% zenv_link @warehouse_order %> + +<% if policy(@warehouse_order).send_to_warehouse? && @warehouse_order.may_mark_dispatched? %> + <%= button_to "send to warehouse!", action: :send_to_warehouse %> +<% end %> + +<% if policy(@warehouse_order).destroy? %> + <%= button_to "delete draft order.", @warehouse_order, method: :delete %> +<% end %> + +
+ <%= link_to "edit this order", edit_warehouse_order_path(@warehouse_order) %>
+ <%= link_to "back to orders", warehouse_orders_path %> +
+ +<%= render partial: "admin_inspector", locals: { record: @warehouse_order } %> + +<% inspector_toggle :line_items %> \ No newline at end of file diff --git a/app/views/warehouse/skus/_form.html.erb b/app/views/warehouse/skus/_form.html.erb new file mode 100644 index 0000000..3325acf --- /dev/null +++ b/app/views/warehouse/skus/_form.html.erb @@ -0,0 +1,52 @@ +<%= form_with(model: warehouse_sku) do |form| %> + <% if warehouse_sku.errors.any? %> +
+

<%= pluralize(warehouse_sku.errors.count, "error") %> prohibited this SKU from being saved:

+ +
    + <% warehouse_sku.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> + +
+ <%= form.label :sku, style: "display: block" %> + <%= form.text_field :sku %> +
+ +
+ <%= form.label :description, style: "display: block" %> + <%= form.textarea :description %> +
+ +
+ <%= form.label :unit_cost, style: "display: block" %> + <%= form.text_field :unit_cost %> +
+ +
+ <%= form.label :customs_description, style: "display: block" %> + <%= form.textarea :customs_description %> +
+ +
+ <%= form.label :in_stock, style: "display: block" %> + <%= form.number_field :in_stock %> +
+ +
+ <%= form.label :ai_enabled, style: "display: block" %> + <%= form.checkbox :ai_enabled %> +
+ +
+ <%= form.label :enabled, style: "display: block" %> + <%= form.checkbox :enabled %> +
+ +
+ <%= form.submit %> +
+<% end %> diff --git a/app/views/warehouse/skus/_sku.html.erb b/app/views/warehouse/skus/_sku.html.erb new file mode 100644 index 0000000..4aa0dd9 --- /dev/null +++ b/app/views/warehouse/skus/_sku.html.erb @@ -0,0 +1,51 @@ +
+
+
+ <%= link_to sku.sku, sku %> + <% if sku.in_stock&.> 0 %> + in stock + <% elsif sku.in_stock&.< 0 %> + <% if sku.inbound&.>= sku.in_stock.abs %> + backordered (more otw) + <% else %> + backordered (!) + <% end %> + <% else %> + not in inventory + <% end %> +
+ +
+
+

+ Name: + <%= sku.name %> +

+

+ In stock: + <%= sku.in_stock %> +

+ <% if sku.description.present? %> +

+ Description: + <%= sku.description %> +

+ <% end %> +

+ Unit cost: + <%= number_to_currency sku.declared_unit_cost %> +

+

+ Customs description: + <%= sku.customs_description %> +

+

+ AI enabled: + <%= render_checkbox sku.ai_enabled %> +

+

+ Enabled: + <%= render_checkbox sku.enabled %> +

+
+
diff --git a/app/views/warehouse/skus/_warehouse_sku.json.jbuilder b/app/views/warehouse/skus/_warehouse_sku.json.jbuilder new file mode 100644 index 0000000..eae2fca --- /dev/null +++ b/app/views/warehouse/skus/_warehouse_sku.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! warehouse_sku, :id, :sku, :description, :declared_unit_cost, :customs_description, :in_stock, :ai_enabled, :enabled, :created_at, :updated_at +json.url warehouse_sku_url(warehouse_sku, format: :json) diff --git a/app/views/warehouse/skus/edit.html.erb b/app/views/warehouse/skus/edit.html.erb new file mode 100644 index 0000000..aeef83b --- /dev/null +++ b/app/views/warehouse/skus/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing sku" %> + +

Editing SKU: <%= @warehouse_sku.sku %>

+ +<%= render "form", warehouse_sku: @warehouse_sku %> + +
+ +
+ <%= link_to "back to this SKU", @warehouse_sku %> | + <%= link_to "back to SKUs", warehouse_skus_path %> +
diff --git a/app/views/warehouse/skus/index.html.erb b/app/views/warehouse/skus/index.html.erb new file mode 100644 index 0000000..76cc548 --- /dev/null +++ b/app/views/warehouse/skus/index.html.erb @@ -0,0 +1,32 @@ +<% content_for :title, "Skus" %> +
+

SKUs

+
+ +
+
+
+
+
+ <% @warehouse_skus.group_by(&:category).each do |cat, skus| %> +
+ + <%= cat.humanize %> + +
+ <% skus.each do |warehouse_sku| %> + <%= render warehouse_sku %> + <% end %> +
+
+ <% end %> +
+
+
+
+ <% admin_tool(element: 'span') do %> + <%= link_to "Create SKU", new_admin_warehouse_sku_path %> + <% end %> +
+
+
diff --git a/app/views/warehouse/skus/index.json.jbuilder b/app/views/warehouse/skus/index.json.jbuilder new file mode 100644 index 0000000..8ef9f57 --- /dev/null +++ b/app/views/warehouse/skus/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @warehouse_skus, partial: "warehouse/skus/warehouse_sku", as: :warehouse_sku diff --git a/app/views/warehouse/skus/new.html.erb b/app/views/warehouse/skus/new.html.erb new file mode 100644 index 0000000..e01a350 --- /dev/null +++ b/app/views/warehouse/skus/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "New sku" %> + +

New sku

+ +<%= render "form", warehouse_sku: @warehouse_sku %> + +
+ +
+ <%= link_to "Back to skus", warehouse_skus_path %> +
diff --git a/app/views/warehouse/skus/show.html.erb b/app/views/warehouse/skus/show.html.erb new file mode 100644 index 0000000..3622ea6 --- /dev/null +++ b/app/views/warehouse/skus/show.html.erb @@ -0,0 +1,14 @@ +<%= render @warehouse_sku %> +<%= render partial: "admin_inspector", locals: { record: @warehouse_sku } %> + +<% zenv_link @warehouse_sku %> + +
+ <% admin_tool do %> + <%= link_to "edit this SKU", edit_admin_warehouse_sku_path(@warehouse_sku) %> + <% end %> + + + <%= link_to "back to SKUs", warehouse_skus_path %> + +
diff --git a/app/views/warehouse/skus/show.json.jbuilder b/app/views/warehouse/skus/show.json.jbuilder new file mode 100644 index 0000000..78e0b79 --- /dev/null +++ b/app/views/warehouse/skus/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "warehouse/skus/warehouse_sku", warehouse_sku: @warehouse_sku diff --git a/app/views/warehouse/templates/_form.html.erb b/app/views/warehouse/templates/_form.html.erb new file mode 100644 index 0000000..809360e --- /dev/null +++ b/app/views/warehouse/templates/_form.html.erb @@ -0,0 +1,41 @@ +<% content_for :head do %> + <%= vite_javascript_tag 'cocoon' %> +<% end %> + +<%= form_with(model: warehouse_template) do |form| %> + <% if warehouse_template.errors.any? %> +
+

<%= pluralize(warehouse_template.errors.count, "error") %> prohibited this template from being saved:

+ +
    + <% warehouse_template.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> + +
+ <%= form.label :name, style: "display: block" %> + <%= form.text_field :name %> +
+ +
+ <%= form.label :public, "share this template with all users?" %> + <%= form.check_box :public %> +
+ +

contents:

+
+ <%= form.fields_for :line_items do |line_item| %> + <%= render 'line_item_fields', :f => line_item %> + <% end %> + +
+ +
+ <%= form.submit class: 'btn success btn-small' %> +
+<% end %> diff --git a/app/views/warehouse/templates/_template.html.erb b/app/views/warehouse/templates/_template.html.erb new file mode 100644 index 0000000..19fcea4 --- /dev/null +++ b/app/views/warehouse/templates/_template.html.erb @@ -0,0 +1,18 @@ +
+
+

<%= link_to template.name, template, class: "text-decoration-none" %>

+
+
+
+
+ User + <%= render 'shared/user_mention', user: template.user %> +
+
+ +
+ Contents: + <%= render 'warehouse/orders/line_items', order: template %> +
+
+
diff --git a/app/views/warehouse/templates/_warehouse_template.json.jbuilder b/app/views/warehouse/templates/_warehouse_template.json.jbuilder new file mode 100644 index 0000000..b5c6086 --- /dev/null +++ b/app/views/warehouse/templates/_warehouse_template.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! warehouse_template, :id, :user_id, :name, :source_tag_id, :created_at, :updated_at +json.url warehouse_template_url(warehouse_template, format: :json) diff --git a/app/views/warehouse/templates/edit.html.erb b/app/views/warehouse/templates/edit.html.erb new file mode 100644 index 0000000..91b3f80 --- /dev/null +++ b/app/views/warehouse/templates/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing template" %> + +

Editing template

+ +<%= render "form", warehouse_template: @warehouse_template %> + +
+ +
+ <%= link_to "Show this template", @warehouse_template %> | + <%= link_to "Back to templates", warehouse_templates_path %> +
diff --git a/app/views/warehouse/templates/index.html.erb b/app/views/warehouse/templates/index.html.erb new file mode 100644 index 0000000..5d09ada --- /dev/null +++ b/app/views/warehouse/templates/index.html.erb @@ -0,0 +1,29 @@ +<% content_for :title, "Templates" %> + +

Order Templates

+ +
+ <%= create_button new_warehouse_template_path, "create template" %> +
+ +

Your templates:

+
+ <% current_user&.warehouse_templates&.each do |warehouse_template| %> + <%= render warehouse_template %> +

+ <%= link_to "view this template", warehouse_template %> +

+ <% end.presence || "none yet!" %> +
+ +

Public templates:

+
+ <% Warehouse::Template.shared.each do |warehouse_template| %> + <%= render warehouse_template %> +

+ <%= link_to "view this template", warehouse_template %> +

+ <% end.presence || "none yet!" %> +
+ +<%= link_to "create template!", new_warehouse_template_path %> diff --git a/app/views/warehouse/templates/index.json.jbuilder b/app/views/warehouse/templates/index.json.jbuilder new file mode 100644 index 0000000..c3a4acd --- /dev/null +++ b/app/views/warehouse/templates/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @warehouse_templates, partial: "warehouse/templates/warehouse_template", as: :warehouse_template diff --git a/app/views/warehouse/templates/new.html.erb b/app/views/warehouse/templates/new.html.erb new file mode 100644 index 0000000..936bfb1 --- /dev/null +++ b/app/views/warehouse/templates/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "New template" %> + +

New template

+ +<%= render "form", warehouse_template: @warehouse_template %> + +
+ +
+ <%= link_to "Back to templates", warehouse_templates_path %> +
diff --git a/app/views/warehouse/templates/show.html.erb b/app/views/warehouse/templates/show.html.erb new file mode 100644 index 0000000..3e24e90 --- /dev/null +++ b/app/views/warehouse/templates/show.html.erb @@ -0,0 +1,10 @@ +

<%= notice %>

+ +<%= render @warehouse_template %> + +
+ <%= link_to "Edit this template", edit_warehouse_template_path(@warehouse_template) %> | + <%= link_to "Back to templates", warehouse_templates_path %> + + <%= button_to "Destroy this template", @warehouse_template, method: :delete %> +
diff --git a/app/views/warehouse/templates/show.json.jbuilder b/app/views/warehouse/templates/show.json.jbuilder new file mode 100644 index 0000000..b377c2a --- /dev/null +++ b/app/views/warehouse/templates/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "warehouse/templates/warehouse_template", warehouse_template: @warehouse_template diff --git a/awa b/awa new file mode 100644 index 0000000..ce575ef --- /dev/null +++ b/awa @@ -0,0 +1,11 @@ +--GB6rO2tUuBXXlNm-LZuY7sA1 +Content-Type: application/json +Content-Disposition: form-data; name="indiciaMetadata" + +{"postage":0.69,"fees":[],"weight":1.0,"SKU":"DFLL0XXXXR00010"} +--GB6rO2tUuBXXlNm-LZuY7sA1 +Content-Type: image/tiff +Content-Disposition: form-data; filename="indiciaImage.tiff"; name="indiciaImage" + +TU0AKgABnuP+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//5AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//oAA//6AAP/+gAD//j8A//4AAP/+gIL/gv+C/4L/gv+C/4L/gv+C/4L/gv+C/8D//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/gv+C/4L/gv+C/8D//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/gv+C/4L/gv+C/8D//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/gv+C/4L/gv+C/8D//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/gv+C/4L/gv+C/8D//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/gv+C/4L/gv+C/8D//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/gv+C/4L/gv+C/8D//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/gv+C/4L/gv+C/8D//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/gv+C/4L/gv+C/8D//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/gv+C/4L/gv+C/8D//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/gv+C/4L/gv+C/8D//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/gv+C/4L/gv+C/8D//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/gv+C/4L/gv+C/8D//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/gv+C/4L/gv+C/8D//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/gv+C/4L/gv+C/8D//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/gv+C/4L/gv+C/8D//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/gv+C/4L/gv+C/8D//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/gv+C/4L/gv+C/8D//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/gv+C/4L/gv+C/8D//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/gv+C/4L/gv+C/8D//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/gv+C/4L/gv+C/8D//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/gv+C/4L/gv+C/8D//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/gv+C/4L/gv+C/8D//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/gv+C/4L/8v/+7IL/gv/S//5/AP/+AAD//oCC/6///tx//7Szs/+trKz/rays/62srP+trKz/rays/62srP+trKz/rays/62srP+trKz/rays/62srP+trKz/rays/62srP+trKz/rays/62srP+trKz/rays/62srP+trKz/rays/62srP+trKz/rays/62srP+trKz/rays/62srP+trKx//62srP+trKz/rays/62srP+trKz/rays/62srP+trKz/rays/62srP+trKz/rays/62srP+trKz/rays/62srP+trKz/rays/62srP+trKz/rays/62srP+trKz/rays/62srP+trKz/rays/62srP+trKz/rays/62srP+trKx//62srP+trKz/rays/62srP+trKz/rays/62srP+trKz/rays/62srP+trKz/rays/62srP+trKz/rays/62srP+trKz/rays/62srP+trKz/rays/62srP+trKz/rays/62srP+trKz/rays/62srP+trKz/rays/62srP+trKxr/62srP+trKz/rays/62srP+trKz/ray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/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIH//Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIH//Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfICj/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/MS0u//7+gv+C/+7//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDo//4AAP/+AAD//gDM//4AAP/+AAD//gDM//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4A5P/+AAD//gAA//4AAP/+9QD//kEA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A8P/+AAD//gAA//4AAP/+AAD//gAA//4A6P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gCc//5/AP/+AAD//oCC/4L/3v/++n//09LS/6GfoP90cnL/R0RF/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIH//Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIH//Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIBf/Ix8g/yMfIP8jHyD/Ix8g/yMfIP9fXFyC/4L/6v/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AOj//gAA//4AAP/+AMz//gAA//4AAP/+AMz//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gDw//7bAP/+8fz//gAA//4AAP/+APz//vUA//5BAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A8P/+AAD//gAA//4AAP/+AAD//gAA//4A6P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gCc//5/AP/+AAD//oCC/4L/yv/+83//xMPE/5eWlv9oZWb/OTU2/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIH//Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIH//Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIAP/jYuMgv+C/+r//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDo//4AAP/+AAD//gDM//4AAP/+AAD//gDM//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+2AD//hkA//44AP/+8QD//gAA//4AAP/+APj//vUA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDw//4AAP/+AAD//gAA//4AAP/+AAD//gAA//6CAP/+i/D//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AnP/+fwD//gAA//6Agv+C/7b/f+jn5/+5uLj/iYeH/1xZWv8uKiv/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/fyMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/ciMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP+8uruC/4L/6v/+AAD//gAA//4AAP/+ALT//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AOT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gD4//7vAP/+GQD//gAA//4AAP/+OAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+gAD//gAA//4AAP/+ANz//oIA//4AAP/+AAD//gDk//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gCC/wH///5/AP/+AAD//oCC/4L/ov9/2djZ/6yrq/9/fX7/TktM/ycjJP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP9/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP9bIx8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP/+64L/gv/q//4AAP/+AAD//gAA//4AtP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4A5P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+APT//o4A//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDg//6CAP/+AAD//gAA//4AAP/+AOT//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AIL/Af///n8A//4AAP/+gIL/gv+S//74f//Qz8//n52e/3Fub/9DQED/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8gf/8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8gR/8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/z46O4L/gv/m//4AAP/+AAD//gAA//4AtP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4A5P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+APD//o4A//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//kEA//4AAP/+AAD//gAA//4AAP/+AOT//oIA//4AAP/+AAD//gAA//4AAP/+AOT//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AIL/Af///n8A//4AAP/+gIL/gv+C//3//vB//8HAwP+TkZL/ZmRk/zQxMf8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyB//yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyAz/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/2xqaoL/gv/m//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AtP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+ANj//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//7x+P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//6A8P/+AAD//gAA//4AAP/+AAD//gAA//4DAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+APT//gAA//4AAP/+AAD//gCc//5/AP/+AAD//oCC/4L/gv/p//7jf/+3tbb/hYOE/1hVVf8sKSr/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8gf/8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8gH/8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/nJqagv+C/+b//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gC0//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4A2P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//jgA//7x/P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+gPT//gAA//4AAP/+AAD//gAA//4DAP/+pgD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AnP/+fwD//gAA//6Agv+C/4L/2f/+/QD//tZ//6imp/97eXn/TEhJ/yUhIv8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyB//yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyAL/yMfIP8jHyD/ysnKgv+C/+b//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gC0//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4A2P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//44AP/+8QD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//kEA//4AAP/+AAD//gAA//6A+P/+AAD//gAA//4AAP/+AwD//qb8//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AJz//n8A//4AAP/+gIL/gv+C/8X/f/b19f/Kycr/nZub/2tpaf8/Ozz/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/fiMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yYjI//29fWC/4L/5v/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AOj//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A3P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AOj//gAA//4AAP/+APT//gAA//4AAP/+AAD//gD8//6OAP/+AAD//gAA//4AAP/+OAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AjP/+fwD//gAA//6Agv+C/4L/sf/+7X//vLq7/46MjP9hXl//MS0u/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIGP/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/TEhJgv+C/+L//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gDo//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+ANz//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDo//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4A+P/+jgD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AjP/+fwD//gAA//6Agv+C/4L/nf/+3X//sK+v/4KAgP9RTk7/KSUm/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIE//Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/3t5eYL/gv/i//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4A6P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDc//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A6P/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+APT//o4A//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AjP/+fwD//gAA//6Agv+C/4L/jf/++n//0dDR/6Cen/9raGn/MzAx/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfID//Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/qqiogv+C/+L//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDA//4AAP/+AAD//gD0//4AAP/+AAD//gDw//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gDo//7YAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//vH4//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+puD//gAA//4AAP/+ANj//gAA//4AAP/+AOj//gAA//4AAP/+AAD//gCc//5/AP/+AAD//oCC/4L/gv+C//j//td//5mXmP9bWFn/JyMk/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyAr/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP/Y19iC/4L/4v/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AMD//gAA//4AAP/+APT//gAA//4AAP/+APD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AOz//tgA//4ZAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//jgA//7x/P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+ANz//gAA//4AAP/+ANj//gAA//4AAP/+AOj//gAA//4AAP/+AAD//gCc//5/AP/+AAD//oCC/4L/gv+C/+z//u9//6yrq/9fXF3/JiIj/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyAc/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/y0qKv/+/IL/gv/i//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AwP/+AAD//gAA//4A9P/+AAD//gAA//4A8P/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4A8P/+5wD//hkA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+/Pj//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+OAD//vEA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A3P/+AAD//gAA//4A2P/+AAD//gAA//4A6P/+AAD//gAA//4AAP/+AJz//n8A//4AAP/+gIL/gv+C/4L/4P9/6ejo/5GPkP84NDX/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8SIx8g/yMfIP8jHyD/Ix8g/1pXWIL/gv/e//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A8P/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AOj//gAA//4AAP/+AAD//gAA//4AAP/+APD//voA//4aAP/+AAD//h4A//4vAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+OAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDQ//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4A6P/+AAD//gAA//4AjP/+fwD//gAA//6Agv/D/wPp6Oj//uMA//7jAP/+4wD//uMA//7jAP/+4wD//uMA//7jAP/+4wD//uMA//7jAP/+4wD//uMA//7jAP/+4wD//uMA//7jAP/+4wD//uMA//7jAP/+4wD//uMA//7jAP/+4wD//uMA//7jAP/+4wD//uMA//7jAP/+4wD//uMA//7jAP/+4wD//uMA//7jAP/+4wD//uMA//7jAP/+4wD//uMA//7jAP/+4wD//uMA//7jAP/+4wD//uMA//7jAP/+4wD//uMA//7jAP/+4wD//uMA//7jAP/+4wD//uMA//7jAP/+4wD//uMA//7jAP/+4wD//uMA//7jAP/+4wD//uMD/+jn56D//vl//6Kgof81MTL/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyAH/yMfIP+Jh4eC/4L/3v/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDo//4AAP/+AAD//gAA//4AAP/+AAD//gDs//6GAP/+AAD//gAA//67AP/+rQD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//l0A//7J+P/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AND//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gDo//4AAP/+AAD//gCM//5/AP/+AAD//oCC/8P/fzMvMP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/fyMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/PiMfIP8jHyD/Ix8g/yMfIP8qJif/Pzs8/z87PP9aV1j/ZWJi/3ZzdP+LiYr/lpSU/62srP/DwsP/3Nvb//X09dD//vF//3Vyc/8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP+4t7eC/4L/3v/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDo//4AAP/+AAD//gAA//4AAP/+AAD//gDs//7tAP/+CwD//gAA//4qAP/+/QD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+NAD//p8A//4AAP/+AAD//gD0//4AAP/+AAD//gAA//6OAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A0P/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AOj//gAA//4AAP/+AIz//n8A//4AAP/+gIL/x/9/397f/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP9/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP9aIx8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP83MzT/VlNU/3p4eP+gnp//ysnK//X09eD/epiWl/8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/+fm54L/gv/e//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A8P/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AOj//gAA//4AAP/+AAD//gAA//4AAP/+AOj//moA//4AAP/+AAD//qIA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4A/P/+jgD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A0P/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AOj//gAA//4AAP/+AIz//n8A//4AAP/+gIL/x/9/sK+v/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP9/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP9mIx8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yYjI/9ST1D/jIqL/9XU1Oj/cnJwcP8/Ozz/RkNE/1pXWP9HREX/Pzs8/z87PP8/Ozz/Pzs8/ywoKf8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP85NjeC/4L/2v/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A0P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//ncA//4RAP/+AAD//gAA//4AAP/+AAD//gAA//5NAP/+t/D//gAA//4AAP/+AAD//qb4//4AAP/+AAD//gAA//4A6P/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AND//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gCc//5/AP/+AAD//oCC/8f/f4KAgP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/fyMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/byMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/R0RF/7GwsP/+/cz//vpH/9jX2P+rqqr/Xltb/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/aGVmgv+C/9r//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AND//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD8//7YAP/+CgD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//iUA//6OAP/+7/z//gAA//4AAP/+APT//gAA//4AAP/+AAD//gDo//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4A0P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AJz//n8A//4AAP/+gIL/x/9/U1BR/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP9/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP92Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/2JfYP/29fXA/zqzsrL/JiMj/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/5eWloL/gv/a//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+APT//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDQ//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+5AD//iUA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4KAP/+ZgD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gDo//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4A0P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AJz//n8A//4AAP/+gIL/y//++n//KiYn/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIH//Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIHf/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP+Rj5C8/zafnZ7/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/xsXFgv+C/9r//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDo//4AAP/+AAD//gDA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A4P/+3wD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD8//4yAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//iUA//4AAP/+AAD//nn4//7HAP/+AAD//gAA//4AAP/+AAD//gAA//4DAP/+pvj//gAA//4AAP/+APD//gAA//4AAP/+ANz//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gCM//5/AP/+AAD//oCC/8v/f9LR0v8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/JiMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/+Df4IL/r/865+bn/zw4Of8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP88ODn/TElK/3h2d//Ew8T0//78MP9APT7/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yUhIv/+84L/gv/a//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A6P/+AAD//gAA//4AwP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AOT//tgA//4ZAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APz//qMA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//558P/+AAD//gAA//4AAP/+DgD//gMA//6m9P/+AAD//gAA//4A8P/+AAD//gAA//4A3P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AIz//n8A//4AAP/+gIL/y/9/pKKi/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8mIx8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/mJaXgv+z/0Lb2tr/QD0+/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/gX9/9P8uhIKD/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP9IRUaC/4L/1v/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AOj//gAA//4AAP/+AMD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDo//7YAP/+GQD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A/P/++gD//hsA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+eez//gAA//4AAP/+AAD//vEA//7H8P/+AAD//gAA//4A8P/+AAD//gAA//4A3P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AIz//n8A//4AAP/+gIL/y/9/dHJy/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8mIx8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/UE1Ngv+///78TP/Lysv/fHl6/yklJv8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD//tb4/y6qqan/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/3d0dYL/gv/W//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4A8P/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4A8P/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//5NAP/++ej//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDo//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AOj//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gCc//5/AP/+AAD//oCC/8v/f0ZCQ/8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/JyMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yQgIf/+44L/0/9i397f/7q5uf+Rj4//YV5f/y4qK/8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/q6qq+P8usbCw/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP+lpKSC/4L/1v/+AAD//gAA//4AAP/+APT//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+APD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+APD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//k0A//757P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AOj//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A6P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AJz//n8A//4AAP/+gIL/z//+8n//JSEi/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfICv/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/5yamoL/4/9ywsHB/01KS/8/Ozz/Ix8g/yMfIP8jHyD/Ix8g/yMfIP83MzT/Pzs8/z87PP9WU1T/WldY/1pXWP9ycHD/dnN0/3ZzdP9+e3z/kY+Q/5GPkP+Rj5D/kY+Q/5GPkP+Rj5D/cG1u/ywpKv8jHyD/Ix8g/7Cur/j/LqCen/8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/1NPTgv+C/9b//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gDw//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gDw//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4eAP/+AQD//gAA//4AAP/+TQD//vnw//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A6P/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDo//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AnP/+fwD//gAA//6Agv/P/3/Ew8T/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/y4jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/1NQUYL/3/8D6Ofn//7jAP/+4wD//uSw/w6enJz/Ix8g/yMfIP/c29v4/y5/fX7/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Kyco//z7/IL/gv/W//4AAP/+AAD//gAA//4A3P/+AAD//gAA//4AAP/+AAD//gAA//4A8P/+AAD//gAA//4A6P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APz//moA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gkA//7v/P/+AAD//gAA//4AAP/+APT//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/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AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AJz//n8A//4AAP/+gIL/5/9/cnBw/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8+Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yUhIv9samr/2NfYtP9/6Ofn/5ORkv8+Ojv/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP9mIx8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8g/yMfIP8jHyD/Ix8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//gAA//4AAP/+AAD//gAA//4A8P/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gDY//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDk//4AAP/+AAD//gD0//4AAP/+AAD//gDo//4AAP/+AAD//gAA//4AnP/+fwD//gAA//6Agv+C/4L/gv+C/4L/gv+9//71AP/+QAD//gAA//4AAP/+BAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+EQD//lcA//58AP/+gAD//mUA//4fAP/+AAD//gAA//4AAP/+OwD//u38//4AAP/+AAD//gDM//4AAP/+AAD//gDo//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gDk//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gCC/wH///5/AP/+AAD//oCC/4L/gv+C/4L/gv+C/7n//vUA//5AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+owD//hgA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4CAP/+cQD//vf4//4AAP/+AAD//gDM//4AAP/+AAD//gDo//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gDk//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gCC/wH///5/AP/+AAD//oCC/4L/gv+C/4L/gv+C/7X//vUA//5AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD8//7zAP/+kQD//jEA//4AAP/+AAD//gAA//4AAP/+AAD//hAA//5iAP/+1fD//gAA//4AAP/+AMz//gAA//4AAP/+AOj//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AOT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AIL/Af///n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/sf/+9QD//kAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A8P/+6QD//r0A//6gAP/+qQD//tEA//766P/+AAD//gAA//4AzP/+AAD//gAA//4A6P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4A5P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4Agv8B///+fwD//gAA//6Agv+C/4L/gv+C/4L/gv/t//7zxP/+9QD//gAA//4AAP/+AAD//gAA//6q4P/+AAD//gAA//4A2P/+AAD//gAA//4A2P/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gDo//4AAP/+AAD//gD0//4AAP/+AAD//gDw//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4A9P/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AJz//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/8f/+2AD//hkA//4PAP/+QgD//nIA//6oAP/+19T//gAA//4AAP/+AAD//gAA//4EAP/+quT//gAA//4AAP/+ANj//gAA//4AAP/+ANj//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4A6P/+AAD//gAA//4A9P/+AAD//gAA//4A8P/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+APT//gAA//4AAP/+APT//gAA//4AAP/+AAD//gCc//5/AP/+AAD//oCC/4L/gv+C/4L/gv+C//X//tgA//4ZAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+DgD//j8A//5xAP/+pQD//tTo//4AAP/+AAD//gAA//4AAP/+AAD//gQA//6q6P/+AAD//gAA//4A2P/+AAD//gAA//4A2P/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gDo//4AAP/+AAD//gD0//4AAP/+AAD//gDw//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4A9P/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AJz//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/9f/+wAD//gsA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gwA//48AP/+cAD//qIA//7TAP/+/gD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AwP/+AAD//gAA//4AAP/+AAD//gAA//4AzP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+ANz//gAA//4AAP/+AOT//gAA//4AAP/+AAD//gAA//4AAP/+AOj//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gCM//5/AP/+AAD//oCC/4L/gv+C/4L/gv+C//H//sAA//4LAP/+AAD//gAA//4BAP/+UAD//jEA//4FAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+CwD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AwP/+AAD//gAA//4AAP/+AAD//gAA//4AzP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+ANz//gAA//4AAP/+AOT//gAA//4AAP/+AAD//gAA//4AAP/+AOj//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gCM//5/AP/+AAD//oCC/4L/gv+C/4L/gv+C/+3//sAA//4LAP/+AAD//gAA//4uAP/+6wD//vgA//7MAP/+lgD//mYA//4yAP/+BgD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+APT//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gDA//4AAP/+AAD//gAA//4AAP/+AAD//gDM//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4A3P/+AAD//gAA//4A5P/+AAD//gAA//4AAP/+AAD//gAA//4A6P/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AIz//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/6f/+wAD//gsA//4AAP/+AAD//i4A//7r8P/++QD//s0A//6YAP/+aAD//jMA//4HAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+ANj//gAA//4AAP/+ALT//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AqP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A8P/+AAD//gAA//4A6P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AJz//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/5f/+wAD//gsA//4AAP/+AAD//i4A//7r4P/++gD//gAA//4AAP/+AAD//gAA//4IAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDY//4AAP/+AAD//gC0//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AKj//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APD//gAA//4AAP/+AOj//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gCc//5/AP/+AAD//oCC/4L/gv+C/4L/gv+C/+H//sAA//4LAP/+AAD//gAA//4uAP/+6+D//gAA//4AAP/+AAD//gAA//77AP/+zwD//pwA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDY//4AAP/+AAD//gC0//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AKj//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APD//gAA//4AAP/+AOj//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gCc//5/AP/+AAD//oCC/4L/gv+C/4L/gv+C/93//sAA//4LAP/+AAD//gAA//4uAP/+6+T//gAA//4AAP/+AAD//gDo//4AAP/+AAD//gD0//4AAP/+AAD//gDw//4AAP/+AAD//gDc//4AAP/+AAD//gAA//4A3P/+AAD//gAA//4A5P/+AAD//gAA//4A9P/+AAD//gAA//4A5P/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4A5P/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AIz//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/2f/+wAD//gsA//4AAP/+AAD//i4A//7r6P/+AAD//gAA//4AAP/+AOj//gAA//4AAP/+APT//gAA//4AAP/+APD//gAA//4AAP/+ANz//gAA//4AAP/+AAD//gDc//4AAP/+AAD//gDk//4AAP/+AAD//gD0//4AAP/+AAD//gDk//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gDk//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AjP/+fwD//gAA//6Agv+C/4L/gv+C/4L/gv/V//7AAP/+CwD//gAA//4AAP/+LgD//uvs//4AAP/+AAD//gAA//4A6P/+AAD//gAA//4A9P/+AAD//gAA//4A8P/+AAD//gAA//4A3P/+AAD//gAA//4AAP/+ANz//gAA//4AAP/+AOT//gAA//4AAP/+APT//gAA//4AAP/+AOT//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AOT//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gCM//5/AP/+AAD//oCC/4L/gv+C/4L/gv+C/9H//sAA//4LAP/+AAD//gAA//4uAP/+6/D//gAA//4AAP/+AAD//gDo//4AAP/+AAD//gD0//4AAP/+AAD//gDw//4AAP/+AAD//gAA//4AAP/+AAD//gDY//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AOj//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gDk//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDo//4AAP/+AAD//gAA//4AnP/+fwD//gAA//6Agv+C/4L/gv+C/4L/gv/N//7AAP/+CwD//gAA//4AAP/+LgD//uv0//4AAP/+AAD//gAA//4A6P/+AAD//gAA//4A9P/+AAD//gAA//4A8P/+AAD//gAA//4AAP/+AAD//gAA//4A2P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gDo//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4A5P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A6P/+AAD//gAA//4AAP/+AJz//n8A//4AAP/+gIL/gv+C/4L/gv+C/4L/yf/+wAD//gsA//4AAP/+AAD//i4A//7r+P/+AAD//gAA//4AAP/+AOj//gAA//4AAP/+APT//gAA//4AAP/+APD//gAA//4AAP/+AAD//gAA//4AAP/+ANj//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4A6P/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AOT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AOj//gAA//4AAP/+AAD//gCc//5/AP/+AAD//oCC/4L/gv+C/4L/gv+C/8X//sAA//4LAP/+AAD//gAA//4uAP/+6/z//gAA//4AAP/+AAD//gDo//4AAP/+AAD//gD0//4AAP/+AAD//gDw//4AAP/+AAD//gAA//4AAP/+AAD//gDY//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AOj//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gDk//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDo//4AAP/+AAD//gAA//4AnP/+fwD//gAA//6Agv+C/4L/gv+C/4L/qv/+4gD//pUA//5nAP/+RQD//kwA//5pAP/+pAD//uy4//7AAP/+CwD//gAA//4AAP/+LgD//usA//4AAP/+AAD//gAA//4A6P/+AAD//gAA//4A6P/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4A6P/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AOj//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4Agv/P//5/AP/+AAD//oCC/4L/gv+C/4L/gv+y//7SAP/+TgD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4IAP/+YQD//uG8//7AAP/+CwD//gAA//4DAP/+sgD//gAA//4AAP/+AAD//gDo//4AAP/+AAD//gDo//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gDo//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4A6P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gCC/8///n8A//4AAP/+gIL/gv+C/4L/gv+C/7b//ogA//4FAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gkA//6RvP/+wAD//g4A//6m/P/+AAD//gAA//4AAP/+AOj//gAA//4AAP/+AOj//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AOj//gAA//4AAP/+APT//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gDo//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AIL/z//+fwD//gAA//6Agv+C/4L/gv+C/4L/vv/+/gD//mYA//4AAP/+AAD//gAA//43AP/+lQD//soA//7fAP/+xwD//o0A//4yAP/+AAD//gAA//4AAP/+AAD//loA//76wP/+7fj//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gDA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A5P/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+APT//gAA//4AAP/+ANz//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gDk//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AnP/+fwD//gAA//6Agv+C/4L/gv+C/4L/vv/+eQD//gAA//4AAP/+EgD//q3k//6yAP/+HwD//gAA//4AAP/+AAD//k0A//78uP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AMD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDk//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4A3P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AOT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gCc//5/AP/+AAD//oCC/4L/gv+C/4L/gv/C//65AP/+AQD//gAA//4SAP/+1dz//u8A//5HAP/+AAD//gAA//4AAP/+Z7j//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gDA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A5P/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+APT//gAA//4AAP/+ANz//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gDk//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AnP/+fwD//gAA//6Agv+C/4L/gv+C/4L/xv/+/AD//icA//4AAP/+AQD//rzU//76AP/+UQD//gAA//4AAP/+AAD//qu8//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4A9P/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AOT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+APD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDo//4AAP/+AAD//gDM//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4Agv8B///+fwD//gAA//6Agv+C/4L/gv+C/4L/xv/+sAD//gAA//4AAP/+Vsz//vcA//46AP/+AAD//gAA//4bAP/+9cD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gD0//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4A5P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4A8P/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AOj//gAA//4AAP/+AMz//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gCC/wH///5/AP/+AAD//oCC/4L/gv+C/4L/gv/G//5dAP/+AAD//gAA//6/yP/+4wD//g0A//4AAP/+AAD//pHA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4A9P/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AOT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+APD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDo//4AAP/+AAD//gDM//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4Agv8B///+fwD//gAA//6Agv+C/4L/gv+C/4L/xv/+JwD//gAA//4HAP/+/MT//o0A//4AAP/+AAD//i7A//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4A2P/+AAD//gAA//4AzP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDo//4AAP/+AAD//gDk//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDw//4AAP/+AAD//gDQ//4AAP/+AAD//gAA//4AnP/+fwD//gAA//6Agv+C/4L/gv+C/4L/xv/+CwD//gAA//4iwP/+8AD//goA//4AAP/+AQD//ubE//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4A2P/+AAD//gAA//4AzP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+APT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDo//4AAP/+AAD//gDk//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDw//4AAP/+AAD//gDQ//4AAP/+AAD//gAA//4AnP/+fwD//gAA//6Agv+C/4L/gv+C/4L/xv/+CAD//gAA//4jvP/+SwD//gAA//4AAP/+t8T//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gDY//4AAP/+AAD//gDM//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AOj//gAA//4AAP/+AOT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+APD//gAA//4AAP/+AND//gAA//4AAP/+AAD//gCc//5/AP/+AAD//oCC/4L/gv+C/4L/gv/G//4eAP/+AAD//gQA//7ywP/+cgD//gAA//4AAP/+osT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gDo//4AAP/+AAD//gAA//4AtP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AzP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AOj//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDY//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gCM//5/AP/+AAD//oCC/4L/gv+C/4L/gv/G//5OAP/+AAD//gAA//6fwP/+dgD//gAA//4AAP/+qsT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gDo//4AAP/+AAD//gAA//4AtP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AzP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AOj//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDY//4AAP/+AAD//gAA//4AAP/+AAD//gD0//4AAP/+AAD//gAA//4AAP/+AAD//gCM//5/AP/+AAD//oCC/4L/gv+C/4L/gv/G//6ZAP/+AAD//gAA//4nAP/++sT//l0A//4AAP/+AAD//s7E//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4A6P/+AAD//gAA//4AAP/+ALT//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AMz//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gDo//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4A2P/+AAD//gAA//4AAP/+AAD//gAA//4A9P/+AAD//gAA//4AAP/+AAD//gAA//4AjP/+fwD//gAA//6Agv+C/4L/gv+C/4L/xv/+8QD//g4A//4AAP/+AAD//n3I//7+AP/+GwD//gAA//4RAP/++8T//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AnP/+fwD//gAA//6Agv+C/4L/gv+C/4L/wv/+hAD//gAA//4AAP/+AgD//rLM//6pAP/+AAD//gAA//5mwP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gCc//5/AP/+AAD//oCC/4L/gv+C/4L/gv/q//70AP/+3wD//toA//7p6P/++QD//i4A//4AAP/+AAD//goA//7B1P/+6wD//h8A//4AAP/+BAD//tnA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AJz//n8A//4AAP/+gIL/gv+C/4L/gv+C//L//rIA//5CAP/+BAD//gAA//4AAP/+AAD//iIA//52AP/+4PD//tkA//4QAP/+AAD//gAA//4IAP/+qdz//vAA//47AP/+AAD//gAA//51vP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gCc//5/AP/+AAD//oCC/4L/gv+C/4L/gv/6//7tAP/+SwD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+BgD//nUA//729P/+wQD//goA//4AAP/+AAD//gEA//5hAP/+7Oj//scA//4oAP/+AAD//gAA//47AP/++IL/gv+C/4L/gv/T//5/AP/+AAD//oCC/4L/gv+C/4L/gv/+//7dAP/+JQD//gAA//4AAP/+AAD//hAA//42AP/+LQD//gcA//4AAP/+AAD//gAA//4AAP/+KwD//tr0//7EAP/+EwD//gAA//4AAP/+AAD//gkA//5aAP/+mQD//r4A//65AP/+jwD//kQA//4AAP/+AAD//gAA//42AP/+7YL/gv+C/4L/gv/P//5/AP/+AAD//oCC/4L/gv+C/4L/g//+2AD//hsA//4AAP/+AAD//hwA//6sAP/++/j//vcA//6rAP/+LAD//gAA//4AAP/+AAD//hQA//7K9P/+4gD//kMA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//l8A//71gv+C/4L/gv+C/8v//n8A//4AAP/+gIL/gv+C/4L/gv+H//7YAP/+GQD//gAA//4AAP/+OwD//uro//78AP/+fgD//gMA//4AAP/+AAD//hIA//7W8P/+swD//kAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+QAD//ryC/4L/gv+C/4L/w//+fwD//gAA//6Agv+C/4L/gv+C/4v//tgA//4ZAP/+AAD//gAA//5FAP/+9dz//qoA//4HAP/+AAD//gAA//4gAP/+6Oz//uMA//6jAP/+fAD//mwA//6AAP/+ogD//uKC/4L/gv+C/4L/u//+fwD//gAA//6Agv+C/4L/gv+C/4///tgA//4ZAP/+AAD//gAA//5GAP/+99T//rMA//4FAP/+AAD//gAA//5Rgv+C/4L/gv+C/4v//n8A//4AAP/+gIL/gv+C/4L/gv+T//7YAP/+GQD//gAA//4AAP/+RgD//vfM//6ZAP/+AAD//gAA//4AAP/+roL/gv+C/4L/gv+P//5/AP/+AAD//oCC/4L/gv+C/4L/l//+2AD//hkA//4AAP/+AAD//kYA//73xP/+VwD//gAA//4AAP/+MoL/gv+C/4L/gv+P//5/AP/+AAD//oCC/4L/gv+C/4L/l//+dgD//gAA//4AAP/+AAD//lgA//78xP/+5QD//goA//4AAP/+AAD//saC/4L/gv+C/4L/k//+fwD//gAA//6Agv+C/4L/gv+C/5f//v4A//5kAP/+AAD//gAA//4AAP/+WAD//vzE//5kAP/+AAD//gAA//5/gv+C/4L/gv+C/5P//n8A//4AAP/+gIL/gv+C/4L/gv+T//7+AP/+ZAD//gAA//4AAP/+AAD//lgA//78yP/+swD//gAA//4AAP/+T4L/gv+C/4L/gv+T//5/AP/+AAD//oCC/4L/gv+C/4L/j//+/gD//mQA//4AAP/+AAD//gAA//5YAP/+/Mz//tkA//4AAP/+AAD//j6C/4L/gv+C/4L/k//+fwD//gAA//6Agv+C/4L/gv+C/4v//v4A//5kAP/+AAD//gAA//4AAP/+WAD//vzQ//7KAP/+AAD//gAA//5Ngv+C/4L/gv+C/5P//n8A//4AAP/+gIL/gv+C/4L/gv+H//7+AP/+ZAD//gAA//4AAP/+AAD//lgA//781P/+hQD//gAA//4AAP/+gIL/gv+C/4L/gv+T//5/AP/+AAD//oCC/4L/gv+C/4L/g//+/gD//mQA//4AAP/+AAD//gAA//5YAP/+/Nz//vMA//4bAP/+AAD//gIA//7Vgv+C/4L/gv+C/5P//n8A//4AAP/+gIL/gv+C/4L/gv+C//7//v4A//5kAP/+AAD//gAA//4AAP/+WAD//vzg//5bAP/+AAD//gAA//5Xgv+C/4L/gv+C/4///n8A//4AAP/+gIL/gv+C/4L/gv+C//r//v4A//5kAP/+AAD//gAA//4AAP/+WAD//vzo//58AP/+AAD//gAA//4WAP/+6IL/gv+C/4L/gv+P//5/AP/+AAD//oCC/4L/gv+C/4L/gv/2//7+AP/+ZAD//gAA//4AAP/+AAD//lgA//788P/+ggD//gAA//4AAP/+BwD//r6C/4L/gv+C/4L/i//+fwD//gAA//6Agv+C/4L/gv+C/4L/8v/+/gD//mQA//4AAP/+AAD//gAA//5YAP/+/Pj//oIA//4AAP/+AAD//gMA//6rgv+C/4L/gv+C/4f//n8A//4AAP/+gIL/gv+C/4L/gv+C/+7//v4A//5kAP/+AAD//gAA//4AAP/+WAD//vwA//6CAP/+AAD//gAA//4DAP/+poL/gv+C/4L/gv+D//5/AP/+AAD//oCC/4L/gv+C/4L/gv/q//7+AP/+ZAD//gAA//4AAP/+AAD//i0A//4AAP/+AAD//gMA//6mgv+C/4L/gv+C/4L//v/+fwD//gAA//6Agv+C/4L/gv+C/4L/5v/+/gD//mQA//4AAP/+AAD//gAA//4AAP/+AwD//qaC/4L/gv+C/4L/gv/6//5/AP/+AAD//oCC/4L/gv+C/4L/gv/i//7+AP/+ZAD//gAA//4AAP/+AwD//qaC/4L/gv+C/4L/gv/2//5/AP/+AAD//oCC/4L/gv+C/4L/gv/e//7+AP/+ZAD//gMA//6mgv+C/4L/gv+C/4L/8v/+fwD//gAA//6Agv+C/4L/gv+C/4L/2v/+/gD//seC/4L/gv+C/4L/gv/u//5/AP/+AAD//oCC/4L/gv+C/4L/gv+C/4L/gv+C/4L/gv/A//5/AP/+AAD//oCC/4L/gv+C/4L/l//+qQD//uuC/4L/gv+C/4L/gv+y//5/AP/+AAD//oCC/4L/gv+C/4L/m//+mgD//gIA//4uAP/+64L/gv+C/4L/gv+C/7b//n8A//4AAP/+gIL/gv+C/4L/gv+f//6aAP/+AgD//gAA//4AAP/+g4L/gv+C/4L/gv+C/7b//n8A//4AAP/+gIL/gv+C/4L/gv+j//6aAP/+AgD//gAA//4AAP/+ZAD//v6C/4L/gv+C/4L/gv+2//5/AP/+AAD//oCC/4L/gv+C/4L/p//+mgD//gIA//4AAP/+AAD//mQA//7+gv+C/4L/gv+C/4L/sv/+fwD//gAA//6Agv+C/4L/gv+C//f//uy4//6aAP/+AgD//gAA//4AAP/+ZAD//v6C/6P//voA//68AP/+oAD//sQA//783P/+7gD//qAA//7e6P/+lAD//or0//7zAP/+sAD//qMA//7Q2P/+3AD//qAA//7x6P/+dQD//qn0//7eAP/+qQD//qAA//7KAP/+/Oj//u4A//6uAP/+pgD//tTk//7uAP/+sAD//qAA//66AP/+9Oz//u8A//7fAP/+3wD//t8A//7fAP/+3wD//t8A//7rgv+P//5/AP/+AAD//oCC/4L/gv+C/4L/+//+2AD//hkA//6CwP/+4AD//gYA//4AAP/+AAD//mQA//7+gv+j//7LAP/+IAD//gAA//4AAP/+AAD//iIA//7O4P/+VgD//gAA//6o6P/+GwD//n34//6qAP/+DwD//gAA//4AAP/+AAD//jsA//7n5P/+9wD//iwA//4AAP/+2ez//uYA//4BAP/+sPz//u8A//5RAP/+AAD//gAA//4AAP/+AAD//iUA//7L8P/+nAD//goA//4AAP/+AAD//gAA//5HAP/+7vT//v4A//56AP/+BwD//gAA//4AAP/+AAD//hEA//6h8P/+ZAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//mGC/4///n8A//4AAP/+gIL/gv+C/4L/gv8B///+2AD//hkA//4AAP/+AAD//qTA//6sAP/+BAD//mQA//7+gv+j//7tAP/+FwD//hMA//6uAP/+3wD//qYA//4PAP/+GAD//u/o//6iAP/+AAD//gAA//6o7P/+0gD//gAA//7F/P/+zAD//gUA//4qAP/+wwD//t4A//6IAP/+AwD//jwA//7+6P/+cAD//gAA//4AAP/+2ez//p8A//4FAP/+8vz//kgA//4AAP/+bgD//tUA//7fAP/+lwD//gsA//4SAP/+5vj//r0A//4CAP/+NAD//skA//7cAP/+fAD//gAA//5O9P/+hwD//gAA//5EAP/+yAD//t8A//61AP/+IgD//gIA//649P/+NAD//hAA//6AAP/+gAD//oAA//6AAP/+gAD//rCC/4///n8A//4AAP/+gIL/gv+C/4L/hP/+2AD//hkA//4AAP/+AAD//kYA//73vP/+ywD//v6C/5///n8A//4AAP/+pfT//qYA//4AAP/+hOz//t8A//4PAP/+FgD//gAA//6o7P/+igD//hAA//78/P/+TQD//gIA//7W9P/+dAD//gAA//637P/+ugD//gEA//4WAP/+AAD//tns//5XAP/+QPz//tAA//4AAP/+UPD//pUA//4AAP/+gvj//joA//4HAP/+4/T//mEA//4AAP/+yvz//vsA//4UAP/+GwD//vb0//7TAP/+AQD//kP4//77AP/+CAD//keC/4L/9v/+fwD//gAA//6Agv+C/4L/gv+I//7YAP/+GQD//gAA//4AAP/+RgD//veC/4L/zv/+LwD//goA//749P/++gD//gwA//4w8P/+/QD//jwA//4lAP/+iwD//gAA//6o7P/+QgD//lT8//71AP/+BwD//jXw//7UAP/+AAD//mLw//7sAP/+GgD//k0A//5kAP/+AAD//tnw//79AP/+EwD//of8//60AP/+IAD//qLw//7QAP/+AAD//l78//7oAP/+AQD//kjw//7BAP/+AAD//nX8//7jAP/+KAD//mvs//4QAP/+H/j//tIA//4AAP/+eoL/gv/2//5/AP/+AAD//oCC/4L/gv+C/4z//tgA//4ZAP/+AAD//gAA//5GAP/+94L/gv/O//76AP/+BAD//jTs//42AP/+AwD//vj0//6FAP/+BAD//sgA//6WAP/+AAD//qjw//7zAP/+BwD//pz8//7LAP/+AAD//mbw//79AP/+BgD//i7w//5TAP/+FgD//ugA//5kAP/+AAD//tnw//7IAP/+AAD//s7g//6gAP/+AAD//nf8//64AP/+AAD//nnw//7wAP/+AAD//kHg//7fAP/+AAD//jn4//6jAP/+AAD//qwA//69AP/+hQD//pEA//7Ugv+H//5/AP/+AAD//oCC/4L/gv+C/5D//tgA//4ZAP/+AAD//gAA//5GAP/+94L/gv/K//7jAP/+AAD//k/s//5PAP/+AAD//uH4//7LAP/+BQD//n38//6WAP/+AAD//qjw//6zAP/+AQD//uL8//6xAP/+AAD//oHs//4dAP/+FPT//p4A//4AAP/+sPz//mQA//4AAP/+2fD//oAA//4YAP/+/uT//vYA//4lAP/+AgD//tD8//6eAP/+AAD//pTs//4KAP/+J+D//lsA//4AAP/+k/j//nIA//4AAP/+HwD//gAA//4AAP/+AAD//gAA//5mAP/++4L/j//+fwD//gAA//6Agv+C/4L/gv+U//7YAP/+GQD//gAA//4AAP/+RgD//veC/4L/xv/+2AD//gAA//5Z7P/+WAD//gAA//7W/P/+9QD//icA//40AP/++/z//pYA//4AAP/+qPD//msA//4s+P/+pQD//gAA//6L7P/+JgD//gr4//7dAP/+DQD//mH4//5kAP/+AAD//tnw//45AP/+XeT//vwA//5RAP/+AAD//nP4//6TAP/+AAD//p7s//4TAP/+HeT//owA//4AAP/+OQD//vr4//5CAP/+AAD//k0A//7KAP/+4wD//r0A//4vAP/+AAD//neC/4///n8A//4AAP/+gIL/gv+C/4L/mP/+2AD//hkA//4AAP/+AAD//kYA//734P/+rAD//rGC/4L/6v/+2wD//gAA//5Y7P/+VwD//gAA//7Y/P/+aAD//gkA//7X+P/+lgD//gAA//6o8P/+JAD//nL4//6pAP/+AAD//ors//4lAP/+C/z//vwA//45AP/+IQD//vL4//5kAP/+AAD//tn0//7tAP/+AwD//qXo//72AP/+TwD//gAA//5UAP/+/Pj//pYA//4AAP/+nez//hIA//4e6P/+hQD//gAA//4pAP/+6fT//sEA//6iAP/+/fT//uoA//4QAP/+CAD//vGC/5P//n8A//4AAP/+gIL/gv+C/4L/nP/+2AD//hkA//4AAP/+AAD//kYA//734P/+rAD//gQA//4GAP/+sYL/gv/u//7oAP/+AAD//k7s//5NAP/+AAD//uUA//6zAP/+AQD//pL0//6WAP/+AAD//qj0//7bAP/+AAD//rr4//62AP/+AAD//oDs//4bAP/+GPz//oEA//4DAP/+wvT//mQA//4AAP/+2fT//qkA//4CAP/+6uz//ugA//43AP/+AAD//mIA//789P/+owD//gAA//6T7P/+CAD//ivw//77AP/+YwD//gAA//41AP/+6tT//lcA//4AAP/+wYL/k//+fwD//gAA//6Agv+C/4L/gv+g//7YAP/+GQD//gAA//4AAP/+RgD//vfg//6sAP/+BAD//gAA//4AAP/+hYL/gv/u//7+AP/+CwD//jPs//4yAP/+BwD//vwA//5gAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+ufz//pMA//4KAP/+9/j//tYA//4AAP/+ZfD//vsA//4EAP/+Nvz//i4A//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//7q/P/+YQD//jXs//7XAP/+IAD//gEA//6C7P/+wwD//gAA//548P/+7AD//gAA//5I9P/+8gD//kQA//4AAP/+TwD//vTQ//5pAP/+AAD//rqC/5P//n8A//4AAP/+gIL/gv+C/4L/pP/+2AD//hkA//4AAP/+AAD//kYA//734P/+rAD//gQA//4AAP/+AAD//oOC/4L/5v/+OwD//ggA//739P/+9gD//gcA//43/P/+nAD//mAA//5gAP/+YAD//mAA//5gAP/+OAD//gAA//4/AP/+YAD//tP8//5MAP/+SfT//vsA//4NAP/+MvD//ssA//4AAP/+afz//n0A//5gAP/+YAD//mAA//5gAP/+YAD//iYA//4AAP/+UgD//mAA//7y/P/+GgD//nzw//7ZAP/+FwD//gcA//6m6P/+8AD//gUA//5F8P/+uAD//gAA//58+P/+9AD//joA//4AAP/+cAD//v3s//7nAP/+MgD//lbs//5CAP/+AAD//t+C/5P//n8A//4AAP/+gIL/gv+C/4L/pP/+QgD//gAA//4AAP/+AAD//pvg//6sAP/+BAD//gAA//4AAP/+g4L/gv/i//6QAP/+AAD//pP0//6RAP/+AAD//o3k//6WAP/+AAD//qj4//75AP/+CwD//pHw//5dAP/+AQD//sX0//5fAP/+AAD//r/k//5kAP/+AAD//tn4//7RAP/+AAD//sP0//72AP/+KQD//gIA//6x4P/+SgD//gQA//7V9P/+TAD//gEA//7S+P/+XgD//gAA//505P/+/gD//h4A//4EAP/+1/T//scA//4CAP/+O4L/j//+fwD//gAA//6Agv+C/4L/gv+k//7oAP/+KQD//gAA//4AAP/+AwD//qLo//6sAP/+BAD//gAA//4AAP/+g4L/gv/e//72AP/+JAD//gcA//6HAP/+vwD//oYA//4GAP/+JQD//vXk//6WAP/+AAD//qj4//68AP/+AAD//tjw//7dAP/+CwD//hUA//6dAP/+vwD//mgA//4AAP/+TeD//mQA//4AAP/+2fj//okA//4QAP/++/T//o8A//4AAP/+DQD//kAA//5AAP/+QAD//kAA//5AAP/+QAD//n74//7PAP/+BgD//h0A//6kAP/+vAD//lwA//4AAP/+YPj//s0A//4BAP/+AAD//j0A//5AAP/+QAD//kAA//5AAP/+QAD//k/4//6nAP/+AQD//hoA//6YAP/+vwD//pAA//4PAP/+DQD//tGC/4///n8A//4AAP/+gIL/gv+C/4L/oP/+6AD//ikA//4AAP/+AAD//gMA//6i8P/+rAD//gQA//4AAP/+AAD//oPY//7dgv+D//7eAP/+PQD//gAA//4AAP/+AAD//jsA//7f4P/+lgD//gAA//6o+P/+dAD//iDo//7DAP/+JQD//gAA//4AAP/+AQD//lgA//7z4P/+ZAD//gAA//7Z+P/+QgD//lLw//5CAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//5T9P/+twD//h4A//4AAP/+AAD//gQA//5kAP/++Pj//oEA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//hT0//6mAP/+GwD//gAA//4AAP/+AAD//jUA//7Ngv+L//5/AP/+AAD//oCC/4L/gv+C/5z//ugA//4pAP/+AAD//gAA//4DAP/+ovj//qwA//4EAP/+AAD//gAA//6D2P/+ggD//gkA//65gv+C//7//uMA//6/AP/+3MT//tEA//7T4P/+1gD//r8A//7pxP/+xQD//t+4//7+AP/+0wD//r8A//7ttP/+0gD//r8A//7mgv+D//5/AP/+AAD//oCC/4L/gv+C/5j//ugA//4pAP/+AAD//gAA//4DAP/+ogD//qwA//4EAP/+AAD//gAA//6D2P/+ggD//gAA//4AAP/+DAD//uqC/4L/gv+C/4L/gv+S//5/AP/+AAD//oCC/4L/gv+C/5T//ugA//4pAP/+AAD//gAA//4DAP/+BAD//gAA//4AAP/+g9j//oIA//4AAP/+AAD//gMA//6mgv+C/4L/gv+C/4L/jv/+fwD//gAA//6Agv+C/4L/gv+Q//7oAP/+KQD//gAA//4AAP/+AAD//gAA//6D2P/+ggD//gAA//4AAP/+AwD//qaC/4L/gv+C/4L/gv+K//5/AP/+AAD//oCC/4L/gv+C/4z//ugA//4pAP/+AAD//gAA//4DAP/+otz//oIA//4AAP/+AAD//gMA//6mgv+C/4L/gv+C/4L/hv/+fwD//gAA//6Agv+C/4L/gv+I//7oAP/+KQD//gAA//4AAP/+AwD//qLk//6CAP/+AAD//gAA//4DAP/+poL/gv+C/4L/gv+C/4L//n8A//4AAP/+gIL/gv+C/4L/hP/+6AD//ikA//4AAP/+AAD//gMA//6i7P/+ggD//gAA//4AAP/+AwD//qaC/4L/gv+C/4L/gv+C//3//n8A//4AAP/+gIL/gv+C/4L/gv8B///+6AD//ikA//4AAP/+AAD//gMA//6i9P/+ggD//gAA//4AAP/+AwD//qaC/4L/gv+C/4L/gv+C//n//n8A//4AAP/+gIL/gv+C/4L/1P/+2AD//kMA//7ptP/+6AD//ikA//4AAP/+AAD//gMA//6i/P/+ggD//gAA//4AAP/+AwD//qaC/4L/gv+C/4L/gv+C//X//n8A//4AAP/+gIL/gv+C/4L/2P/+2AD//hkA//4AAP/+KwD//um0//7oAP/+KQD//gAA//4AAP/+AwD//lIA//4AAP/+AAD//gMA//6mgv+C/+7//skA//6gAP/+oAD//uDw//78AP/+oAD//qAA//6u2P/+9AD//rAA//6AAP/+fwD//oAA//6sAP/+88T//vcA//6gAP/+oAD//qAA//6gAP/+owD//r8A//6/AP/+zwD//vzk//72AP/+tgD//oQA//5/AP/+gAD//qoA//7w4P/+yAD//okA//5/AP/+gAD//pYA//7d8P/+qwD//qAA//6gAP/+oAD//qAA//6gAP/+oAD//qAA//6gAP/+oAD//qAA//7P7P/+yQD//qAA//6gAP/+x9j//ukA//6lAP/+gAD//n4A//6AAP/+qwD//vDs//7CAP/+oAD//qAA//6gAP/+oAD//qAA//6gAP/+oAD//qAA//6gAP/+orj//n8A//4AAP/+gIL/gv+C/4L/2P/+pQD//gIA//4AAP/+AAD//isA//7ptP/+6AD//ikA//4AAP/+AAD//gAA//4AAP/+AwD//qaC/4L/6v/+cAD//gAA//4AAP/+rPD//vUA//4AAP/+AAD//ibc//6rAP/+EgD//gAA//4AAP/+AAD//gAA//4AAP/+DwD//qTI//7oAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4cAP/+uOz//poA//4SAP/+AAD//gAA//4AAP/+AAD//gAA//4MAP/+gAD//vzw//7gAP/+NgD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//mEA//74+P/+HgD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//6A7P/+KwD//gAA//4AAP/+JAD//v7k//74AP/+cwD//gcA//4AAP/+AAD//gAA//4AAP/+AAD//gkA//6I8P/+XQD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//ga4//5/AP/+AAD//oCC/4L/gv+C/9T//p8A//4CAP/+AAD//gAA//4rAP/+6bT//ugA//4pAP/+AAD//gAA//4DAP/+poL/gv/m//5wAP/+AAD//gAA//6s8P/+9QD//gAA//4AAP/+JuD//tEA//4EAP/+AAD//gAA//4SAP/+IAD//g4A//4AAP/+AAD//gIA//68zP/+6AD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gcA//7Q9P/+gAD//gAA//4AAP/+AAD//gAA//4UAP/+AAD//gAA//4AAP/+AAD//lEA//79+P/+/gD//jAA//4AAP/+AAD//gcA//4gAP/+GQD//gAA//4AAP/+AAD//mX4//4eAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//oDw//7GAP/+AAD//gAA//4AAP/+AAD//rvo//73AP/+QAD//gAA//4AAP/+AAD//gAA//4YAP/+AAD//gAA//4AAP/+AAD//n30//5dAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+Brj//n8A//4AAP/+gIL/gv+C/4L/0P/+nwD//gIA//4AAP/+AAD//isA//7ptP/+6AD//ikA//4DAP/+poL/gv/i//5wAP/+AAD//gAA//6s8P/+9QD//gAA//4AAP/+JuD//mcA//4AAP/+AAD//pf4//75AP/+aQD//gAA//4AAP/+Rsz//ugA//4AAP/+AAD//i0A//7fAP/+3wD//t8A//62AP/+KAD//gAA//4AAP/+Xvj//r4A//4BAP/+AAD//gIA//57AP/+7Pz//vYA//6gAP/+DgD//gAA//4AAP/+ivj//sAA//4AAP/+AAD//kwA//7x+P/+uAD//gUA//4AAP/+AwD//un8//7jAP/+3wD//t8A//7fAP/+UwD//gAA//4AAP/+pQD//t8A//7fAP/+3wD//u/w//5jAP/+AAD//gAA//4AAP/+AAD//lXo//5zAP/+AAD//gAA//4YAP/+qgD//vj8//7tAP/+bQD//gAA//4AAP/+BQD//t74//5dAP/+AAD//gAA//6nAP/+3wD//t8A//7fAP/+3wD//t8A//7fAP/+4Lj//n8A//4AAP/+gIL/gv+C/4L/zP/+nwD//gIA//4AAP/+AAD//isA//7ptP/+6AD//rCC/4L/3v/+cAD//gAA//4AAP/+rPD//vUA//4AAP/+AAD//ibg//5GAP/+AAD//gAA//7m9P/+8QD//iIA//4gAP/+UMz//ugA//4AAP/+AAD//jPw//7CAP/+AAD//gAA//4r+P/+PAD//gAA//4AAP/+g+z//rwA//4AAP/+AAD//hQA//73/P/+nwD//gAA//4AAP/+jPD//mEA//4gAP/+MwD//s/s//5fAP/+AAD//gAA//695P/+8wD//gwA//4AAP/+MgD//igA//4AAP/+BgD//ujw//7oAP/+BwD//gAA//4JAP/+1ez//k8A//4CAP/+LAD//rX4//5dAP/+AAD//gAA//6/nP/+fwD//gAA//6Agv+C/4L/gv/I//6fAP/+AgD//gAA//4AAP/+KwD//unI//6bAP/+7YL/gv/O//5wAP/+AAD//gAA//6s8P/+9QD//gAA//4AAP/+JuD//moA//4AAP/+AAD//ikA//6cAP/+6bj//ugA//4AAP/+AAD//jPw//7VAP/+AAD//gAA//4r/P/+6wD//gEA//4AAP/+DQD//vXo//45AP/+AAD//gAA//69/P/+wwD//gAA//4AAP/+CwD//noA//7Q1P/+XwD//gAA//4AAP/+veT//pwA//4AAP/+AAD//o8A//6GAP/+AAD//gAA//6I8P/+mgD//gAA//4AAP/+X+j//vIA//728P/+XQD//gAA//4AAP/+v5z//n8A//4AAP/+gIL/gv+C/4L/7P/+5QD//pgA//6AAP/+iAD//r7s//6fAP/+AgD//gAA//4AAP/+KwD//unQ//6CAP/+AAD//jIA//7tgv+C/9L//nAA//4AAP/+AAD//qzw//71AP/+AAD//gAA//4m4P/+2AD//goA//4AAP/+AAD//gAA//4AAP/+LAD//nIA//7KxP/+6AD//gAA//4AAP/+M/T//vIA//5RAP/+AAD//gAA//5U/P/+wQD//gAA//4AAP/+QOT//nUA//4AAP/+AAD//oz8//7+AP/+PQD//gAA//4AAP/+AAD//gAA//4WAP/+WQD//qcA//745P/+XwD//gAA//4AAP/+veT//jgA//4AAP/+BAD//ugA//7iAP/+AgD//gAA//4jAP/+/vT//mgA//4AAP/+AAD//pzQ//5dAP/+AAD//gAA//4wAP/+QAD//kAA//5AAP/+QAD//kAA//5AAP/+q7j//n8A//4AAP/+gIL/gv+C/4L/9P/+9QD//nUA//4GAP/+AAD//gAA//4AAP/+AAD//i4A//7G8P/+nwD//gIA//4AAP/+AAD//isA//7p2P/+ggD//gAA//4AAP/+AwD//rSC/4L/0v/+cAD//gAA//4AAP/+rPD//vUA//4AAP/+AAD//ibc//6/AP/+HgD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//k4A//7uzP/+6AD//gAA//4AAP/+BwD//iAA//4gAP/+IAD//gYA//4AAP/+AAD//gEA//6//P/+rgD//gAA//4AAP/+VuT//osA//4AAP/+AAD//nn4//7sAP/+SgD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//h8A//7E6P/+XwD//gAA//4AAP/+vej//tQA//4AAP/+AAD//kr4//5EAP/+AAD//gAA//669P/+UwD//gAA//4AAP/+sfT//qsA//5gAP/+YAD//mAA//5gAP/+YAD//pD4//5dAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+j7j//n8A//4AAP/+gIL/gv+C/4L/+P/+4QD//i0A//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+BgD//qnw//6fAP/+AgD//gAA//4AAP/+KwD//ung//6CAP/+AAD//gAA//4DAP/+poL/gv/O//5xAP/+AAD//gAA//6r8P/+9QD//gAA//4AAP/+J9j//vkA//6rAP/+XQD//hkA//4AAP/+AAD//gAA//4AAP/+SMz//ugA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gcA//6V+P/+tAD//gAA//4AAP/+UeT//oUA//4AAP/+AAD//n7w//7MAP/+dwD//jAA//4AAP/+AAD//gAA//4AAP/+DwD//t/s//5fAP/+AAD//gAA//696P/+cAD//gAA//4AAP/+p/j//qMA//4AAP/+AAD//lT0//5YAP/+AAD//gAA//6p9P/+eAD//gAA//4AAP/+AAD//gAA//4AAP/+Tfj//l0A//4AAP/+AAD//jAA//5AAP/+QAD//kAA//5AAP/+QAD//kAA//6ruP/+fwD//gAA//6Agv+C/4L/gv/8//7ZAP/+HAD//gAA//4AAP/+EQD//mkA//6AAP/+TwD//gQA//4AAP/+AAD//ggA//7L8P/+nwD//gIA//4AAP/+AAD//isA//7p6P/+ggD//gAA//4AAP/+AwD//qaC/4L/yv/+dgD//gAA//4AAP/+qPD//vIA//4AAP/+AAD//izI//7WAP/+dwD//gQA//4AAP/+AAD//tHQ//7oAP/+AAD//gAA//4TAP/+YAD//mAA//5gAP/+fgD//pUA//7j9P/+ywD//gAA//4AAP/+LuT//mIA//4AAP/+AAD//pbk//7sAP/+nwD//iAA//4AAP/+AAD//njs//5fAP/+AAD//gAA//697P/++AD//hQA//4AAP/+AAD//lcA//5gAP/+YAD//lYA//4AAP/+AAD//gUA//7o+P/+eQD//gAA//4AAP/+gvT//poA//5AAP/+QAD//jIA//4AAP/+AAD//k34//5dAP/+AAD//gAA//6/nP/+fwD//gAA//6Agv+C/4L/gv8A//7YAP/+GQD//gAA//4AAP/+OgD//uf0//7KAP/+FwD//gAA//4AAP/+OOz//p8A//4CAP/+AAD//gAA//4rAP/+6fD//oIA//4AAP/+AAD//gMA//6mgv+C/8b//oUA//4AAP/+AAD//pnw//7jAP/+AAD//gAA//445P/+3QD//kAA//4pAP/+bOz//mMA//4AAP/+AAD//qjQ//7oAP/+AAD//gAA//4z3P/++gD//goA//4AAP/+AQD//tns//75AP/+GAD//gAA//4AAP/+z/z//mEA//40AP/+JQD//vfw//68AP/+AAD//gAA//5P7P/+XwD//gAA//4AAP/+vez//qkA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+h/j//rkA//4AAP/+AAD//i8A//7+7P/+xQD//gAA//4AAP/+Tfj//l0A//4AAP/+AAD//r+c//5/AP/+AAD//oCC/4L/gv+F//7YAP/+GQD//gAA//4AAP/+RQD//vbs//7HAP/+AgD//gAA//4AAP/+0ez//p8A//4CAP/+AAD//gAA//4rAP/+6fj//oIA//4AAP/+AAD//gMA//6mgv+C/8L//qQA//4AAP/+AAD//kQA//7+9P/+owD//gAA//4AAP/+VOT//vEA//4GAP/+AAD//gwA//7k8P/+QwD//gAA//4AAP/+u9D//ugA//4AAP/+AAD//jPY//5hAP/+AAD//gAA//4/AP/+9vD//m0A//4AAP/+AAD//i4A//7+/P/+UgD//gAA//4AAP/+lvD//pwA//4AAP/+AAD//mLs//5fAP/+AAD//gAA//697P/+RQD//gAA//4AAP/+OQD//kAA//5AAP/+QAD//kAA//48AP/+AAD//gAA//4iAP/+/vz//vwA//4hAP/+AAD//gAA//6G7P/+nwD//gAA//4AAP/+Tfj//l0A//4AAP/+AAD//r+c//5/AP/+AAD//oCC/4L/gv+J//7YAP/+GQD//gAA//4AAP/+RgD//vfk//5MAP/+AAD//gAA//6X6P/+nwD//gIA//4AAP/+AAD//isA//7pAP/+ggD//gAA//4AAP/+AwD//qaC/4L/vv/+5wD//gYA//4AAP/+AAD//lIA//6zAP/+vwD//o4A//4OAP/+AAD//gAA//6c+P/+6QD//kAA//5AAP/+iPj//loA//4AAP/+AAD//iMA//6hAP/+vwD//rgA//5cAP/+AAD//gAA//4TAP/+9Pz//tUA//5AAP/+QAD//pzk//7oAP/+AAD//gAA//4z2P/+3wD//hEA//4AAP/+AAD//ikA//6bAP/+vwD//qoA//5HAP/+AAD//gAA//4CAP/+uvj//rMA//4AAP/+AAD//gcA//58AP/+vwD//r8A//6MAP/+DAD//gAA//4AAP/+ruz//l8A//4AAP/+AAD//r3w//7fAP/+AgD//gAA//4gAP/+/uz//iwA//4AAP/+AAD//rr4//6wAP/+AQD//gAA//4AAP/+UAD//q0A//6/AP/+nwD//kcA//4AAP/+AAD//gAA//5N+P/+XQD//gAA//4AAP/+dwD//qAA//6gAP/+oAD//qAA//6gAP/+oAD//qAA//7nvP/+fwD//gAA//6Agv+C/4L/jf/+2AD//hkA//4AAP/+AAD//kYA//734P/+eAD//gAA//4AAP/+f+T//p8A//4CAP/+AAD//gAA//4WAP/+AAD//gAA//4DAP/+poL/gv+2//6KAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//41AP/++fj//uEA//4AAP/+AAD//mH4//7lAP/+HgD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4BAP/+nvj//sYA//4AAP/+AAD//nvk//7oAP/+AAD//gAA//4z1P/+vgD//hAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+BAD//pfw//5cAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//5GAP/+/ez//l8A//4AAP/+AAD//r3w//59AP/+AAD//gAA//576P/+jgD//gAA//4AAP/+U/T//pAA//4EAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4jAP/+x/j//l0A//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+v7z//n8A//4AAP/+gIL/gv+C/5H//tgA//4ZAP/+AAD//gAA//5GAP/+99z//lUA//4AAP/+AAD//p3g//6fAP/+AgD//gAA//4AAP/+AAD//gMA//6mgv+C/67//qUA//4iAP/+AAD//gAA//4AAP/+AAD//goA//5qAP/+8vT//uEA//4AAP/+AAD//mH0//7mAP/+XgD//gcA//4AAP/+AAD//gAA//4AAP/+MAD//rH0//7GAP/+AAD//gAA//575P/+6AD//gAA//4AAP/+M9D//t8A//5iAP/+DQD//gAA//4AAP/+AAD//gYA//5KAP/+yez//vwA//6OAP/+GwD//gAA//4AAP/+AAD//gAA//4VAP/+ewD//vfo//5fAP/+AAD//gAA//699P/+/AD//h0A//4AAP/+AAD//tjo//7qAP/+BgD//gAA//4FAP/+5/T//swA//5QAP/+CAD//gAA//4AAP/+AAD//gYA//5CAP/+nAD//vn0//5dAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//r+8//5/AP/+AAD//oCC/4L/gv+V//7YAP/+GQD//gAA//4AAP/+RQD//vfc//7fAP/+BwD//gAA//4HAP/+6Nz//p8A//4CAP/+AAD//gMA//6mgv+C/6L//uYA//7CAP/+vwD//tIA//74yP/++QD//tEA//6/AP/+xgD//uuC//f//vsA//7cAP/+vwD//tQA//702P/+4gD//r8A//6/AP/+24L/6//++AD//s8A//6/AP/+2QD//vOC//P//n8A//4AAP/+gIL/gv+C/5X//qQA//4CAP/+AAD//gAA//4sAP/+6uD//vkA//49AP/+AAD//gAA//5v1P/+nwD//gYA//6mgv+C/4L/gv+C/4L/gv+x//5/AP/+AAD//oCC/4L/gv+R//6eAP/+AgD//gAA//4AAP/+LAD//uro//78AP/+VgD//gAA//4AAP/+JQD//vPQ//7ggv+C/4L/gv+C/4L/gv+t//5/AP/+AAD//oCC/4L/gv+N//6eAP/+AgD//gAA//4AAP/+LAD//urw//78AP/+WQD//gAA//4AAP/+EQD//taC/4L/gv+C/4L/gv+C/4L/9P/+fwD//gAA//6Agv+C/4L/pf/+3wD//vns//6eAP/+AgD//gAA//4AAP/+LAD//ur4//78AP/+WQD//gAA//4AAP/+DgD//siC/4L/gv+C/4L/gv+C/4L/8P/+fwD//gAA//6Agv+C/4L/qf/+2AD//hkA//5OAP/++ez//p4A//4CAP/+AAD//gAA//4sAP/+6gD//vwA//5ZAP/+AAD//gAA//4OAP/+x4L/gv+C/4L/gv+C/4L/gv/s//5/AP/+AAD//oCC/4L/gv+t//7YAP/+GQD//gAA//4AAP/+TgD//vns//6eAP/+AgD//gAA//4AAP/+LAD//lUA//4AAP/+AAD//g4A//7Hgv+C/4L/gv+C/4L/gv+C/+j//n8A//4AAP/+gIL/gv+C/7H//v4A//4iAP/+AAD//gAA//4AAP/+AAD//k4A//757P/+ngD//gIA//4AAP/+AAD//gAA//4AAP/+DgD//seC/4L/gv+C/4L/gv+C/4L/5P/+fwD//gAA//6Agv+C/4L/rf/+bgD//gAA//4AAP/+AAD//gAA//4AAP/+TgD//vns//6eAP/+AgD//gAA//4AAP/+AAD//rGC/4L/gv/R//78AP/+YAD//v2C/4L/gv+C/5z//n8A//4AAP/+gIL/gv+C/63//uQA//4HAP/+AAD//h4A//4BAP/+AAD//gAA//5OAP/++ez//p4A//4CAP/+AAD//gAA//4sAP/+6oL/gv+C/93//u8A//6IAP/+RwD//gAA//5HAP/+kAD//vfs//7+AP/+wwD//qAA//68AP/++ND//v4A//7LAP/+oQD//q8A//7y6P/+8AD//rEA//6gAP/+z4L/gv+C/7n//n8A//4AAP/+gIL/gv+C/6n//mgA//4AAP/+EwD//ocA//4BAP/+AAD//gAA//5OAP/++ez//p4A//4CAP/+AAD//gAA//4sAP/+6oL/gv+C/+X//ucA//4jAP/+AAD//i0A//4AAP/+IwD//gAA//43AP/+9vT//t0A//4uAP/+AAD//gAA//4AAP/+FgD//rvY//7OAP/+JwD//gAA//4AAP/+AAD//g0A//6k8P/+kwD//gwA//4AAP/+AAD//gAA//43AP/+24L/gv+C/8H//n8A//4AAP/+gIL/gv+C/6n//uAA//4FAP/+AAD//osA//6TAP/+AQD//gAA//4AAP/+TgD//vns//6eAP/+AgD//gAA//4AAP/+LAD//uqC/4L/gv/p//5iAP/+AAD//o8A//76AP/+AAD//vkA//5gAP/+AAD//ob4//75AP/+KgD//ggA//6dAP/+3wD//rYA//4bAP/+DAD//t3g//7kAP/+EQD//hAA//6oAP/+3wD//roA//4gAP/+AwD//sz4//6hAP/+AAD//iEA//61AP/+3wD//q4A//4XAP/+HAD//uyC/4L/gv/F//5/AP/+AAD//oCC/4L/gv+l//5iAP/+AAD//hgA//73AP/+kwD//gEA//4AAP/+AAD//k4A//757P/+ngD//gIA//4AAP/+AAD//iwA//7qgv+C/4L/7f/+FwD//h/8//76AP/+AAD//vsA//7iAP/+EgD//mX4//6fAP/+AAD//ob0//7FAP/+AAD//mXg//5iAP/+AAD//rz0//63AP/+AAD//mb8//7+AP/+HAD//gcA//7c9P/+wgD//gAA//52gv+C/4L/xf/+fwD//gAA//6Agv+C/4L/pf/+2wD//gQA//4AAP/+k/z//pMA//4BAP/+AAD//gAA//5OAP/++ez//p4A//4CAP/+AAD//gAA//4sAP/+6oL/gv+C//H//g4A//4o/P/++gD//gAA//777P/+TgD//gAA//7j8P/+JwD//hMA//796P/++AD//g0A//5B8P/+/AD//rsA//7Q/P/+2QD//gAA//5O7P/+IAD//iKC/4L/gv/F//5/AP/+AAD//oCC/4L/gv+h//5cAP/+AAD//h0A//76/P/+kwD//gEA//4AAP/+AAD//k4A//757P/+ngD//gIA//4AAP/+AAD//iwA//7qgv+C/4L/9f/+QAD//gAA//6yAP/++gD//gAA//777P/+HgD//hXs//5WAP/+AAD//tvo//7NAP/+AAD//oT8//7tAP/+vwD//skA//789P/+xwD//gAA//5r7P/+NAD//gEA//7zgv+C/4L/yf/+fwD//gAA//6Agv+C/4L/of/+1wD//gIA//4AAP/+m/j//pMA//4BAP/+AAD//gAA//5OAP/++ez//p4A//4CAP/+AAD//gMA//6xgv+C/4L/9f/+xAD//gYA//4EAP/+UgD//gAA//7f8P/+/gD//gUA//4w7P/+bgD//gAA//7B6P/+pwD//gAA//6fAP/+bgD//gYA//4AAP/+AAD//h8A//6++P/+3QD//gAA//5K8P/+/AD//hQA//4AAP/+3IL/gv+C/8n//n8A//4AAP/+gIL/gv+C/9X//vfM//5WAP/+AAD//iMA//77+P/+kwD//gEA//4AAP/+AAD//k4A//757P/+ngD//gUA//6mgv+C/4L/7f/+zAD//kcA//4AAP/+AAD//gAA//4vAP/+sfj//vcA//4AAP/+Ouz//ncA//4AAP/+t+j//pkA//4AAP/+JwD//hYA//6OAP/+vQD//n8A//4JAP/+CQD//tL4//4mAP/+AwD//sP0//6WAP/+AAD//gAA//7Vgv+C/4L/yf/+fwD//gAA//6Agv+C/4L/2f/+2AD//hkA//5MAP/+y9T//tIA//4BAP/+AAD//qP0//6TAP/+AQD//gAA//4AAP/+TgD//vns//7fgv+C/4L/4f/+5QD//gAA//5YAP/+BQD//gAA//6g/P/++wD//gAA//457P/+dgD//gAA//656P/+kgD//gAA//4HAP/+1/T//q0A//4AAP/+U/j//rYA//4DAP/+CQD//moA//6WAP/+XAD//ggA//40AP/+AAD//tqC/4L/gv/J//5/AP/+AAD//oCC/4L/gv/d//7YAP/+GQD//gAA//4AAP/+AAD//ksA//7L2P/+UQD//gAA//4pAP/+/fT//pMA//4BAP/+AAD//gAA//5OAP/++YL/gv+C/83//voA//4AAP/++wD//swA//4HAP/+FwD//vz8//4IAP/+Luz//mwA//4AAP/+xuj//p4A//4AAP/+UOz//hkA//4S9P/+tgD//iYA//4AAP/+AAD//i8A//7BAP/+XAD//gAA//7tgv+C/4L/yf/+fwD//gAA//6Agv+C/4L/4f/+2AD//hkA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//5LAP/+yuD//s0A//4BAP/+AAD//qvw//6TAP/+AQD//gAA//4AAP/+TgD//vmC/4L/gv/d//7fAP/+3vz//voA//4AAP/++/z//kwA//4AAP/+2fz//ikA//4U7P/+UQD//gAA//7j6P/+ugD//gAA//5l7P/+MwD//gXs//7kAP/+6/j//jMA//4Rgv+C/4L/xf/+fwD//gAA//6Agv+C/4L/4f/+wQD//gsA//4AAP/+AAD//gEA//4sAP/+AAD//gAA//4AAP/+AAD//koA//7J5P/+SwD//gAA//4wAP/+/vD//pMA//4BAP/+AAD//gAA//5Rgv+C/4L/4f/+zAD//gAA//5Z/P/++gD//gAA//77/P/+VAD//gAA//7c/P/+WwD//gAA//7f8P/+HgD//hjk//7rAP/+AwD//j3w//7+AP/+FwD//iL4//6kAP/+qfD//ugA//4DAP/+TYL/gv+C/8X//n8A//4AAP/+gIL/gv+C/93//sAA//4LAP/+AAD//gAA//4oAP/+rgD//kgA//4AAP/+AAD//gAA//4AAP/+SQD//sjs//7HAP/+AAD//gAA//6z7P/+kwD//gEA//4DAP/+poL/gv+C/+H//vgA//4TAP/+DAD//uUA//76AP/+AAD//vsA//7rAP/+EQD//hoA//7+/P/+rwD//gAA//509P/+sQD//gAA//5u4P/+SAD//gIA//7F9P/+rAD//gAA//5y+P/+NQD//gwA//7u9P/+bAD//gAA//6wgv+C/4L/xf/+fwD//gAA//6Agv+C/4L/2f/+wAD//gsA//4AAP/+AAD//igA//7nAP/+yQD//kwA//4AAP/+AAD//gAA//4AAP/+SAD//sjw//5FAP/+AAD//jfo//6TAP/+poL/gv+C/9n//pAA//4AAP/+KAD//qoA//4AAP/+qwD//i0A//4AAP/+nfj//v4A//48AP/+AgD//nQA//6/AP/+lgD//g4A//4UAP/+5vT//hAA//4AAP/+7vj//tYA//4NAP/+EAD//pIA//6/AP/+jgD//gsA//4YAP/+6/j//qUA//4AAP/+NwD//rIA//68AP/+YgD//gAA//5Pgv+C/4L/wf/+fwD//gAA//6Agv+C/4L/1f/+wAD//gsA//4AAP/+AAD//igA//7n/P/+zQD//k8A//4AAP/+AAD//gAA//4AAP/+SAD//sf4//7BAP/+AAD//gAA//66gv+C/4L/uf/+iQD//hEA//4AAP/+AAD//gAA//4RAP/+mPD//uwA//5OAP/+AAD//gAA//4AAP/+KwD//s/w//4QAP/+AAD//u70//7NAP/+NQD//gAA//4AAP/+AAD//jwA//7b8P/+jgD//g0A//4AAP/+AAD//ggA//5uAP/++IL/gv+C/8H//n8A//4AAP/+gIL/gv+C/9H//sAA//4LAP/+AAD//gAA//4oAP/+5/j//tAA//5TAP/+AQD//gAA//4AAP/+AAD//kcA//7H/P/+QQD//gAA//4/gv+C/4L/tf/++AD//sgA//4AAP/+xAD//vjk//7rAP/+vwD//tTI//7jAP/+vwD//uXk//72AP/+xwD//sUA//7zgv+C/4L/uf/+fwD//gAA//6Agv+C/4L/zf/+wAD//gsA//4AAP/+AAD//igA//7n9P/+0wD//lYA//4CAP/+AAD//gAA//4AAP/+SQD//o8A//4AAP/+AAD//sKC/4L/gv+1//76AP/+AAD//vuC/4L/gv+C/5z//n8A//4AAP/+gIL/gv+C/8n//sAA//4LAP/+AAD//gAA//4oAP/+5/D//tYA//5aAP/+AwD//gAA//4AAP/+AAD//gIA//4AAP/+RoL/gv+C/7H//t+C/4L/gv+C/5j//n8A//4AAP/+gIL/gv+C/8X//sAA//4LAP/+AAD//gAA//4oAP/+5+z//tkA//5eAP/+BAD//gAA//4AAP/+AAD//gQA//7ngv+C/4L/gv+C/4L/gv+C/8j//n8A//4AAP/+gIL/gv+G//7YAP/+hQD//ujI//7AAP/+CwD//gAA//4AAP/+KAD//ufo//7bAP/+YgD//gUA//4DAP/+poL/gv+C/4L/gv+C/4L/gv/E//5/AP/+AAD//oCC/4L/iv/+2AD//hkA//4AAP/+BgD//lkA//7GzP/+wAD//gsA//4AAP/+AAD//igA//7n5P/+3gD//rqC/4L/gv+C/4L/gv+C/4L/wP/+fwD//gAA//6Agv+C/47//u0A//4ZAP/+AAD//gAA//4AAP/+AAD//gAA//4xAP/+mwD//vbU//7AAP/+CwD//gAA//4AAP/+KAD//ueC/4L/gv+C/4L/gv+C/4L/oP/+fwD//gAA//6Agv+C/47//v4A//4qAP/+AAD//hgA//5DAP/+AAD//gAA//4AAP/+AAD//hEA//5yAP/+3Nj//sAA//4LAP/+AAD//gAA//4oAP/+54L/gv+C/4L/gv+C/4L/gv+k//5/AP/+AAD//oCC/4L/iv/+mgD//gAA//4AAP/+pQD//sIA//5HAP/+AAD//gAA//4AAP/+AAD//gEA//5KAP/+tAD//v7g//7AAP/+CwD//gAA//4AAP/+KAD//ueC/4L/gv+C/4L/gv+C/4L/qP/+fwD//gAA//6Agv+C/4r//vcA//4WAP/+AAD//hoA//72/P/+zwD//loA//4FAP/+AAD//gAA//4AAP/+AAD//iIA//6LAP/+7eT//sAA//4LAP/+AAD//gAA//4oAP/+54L/gv+C/6n//uIA//6/AP/+vwD//uTg//7iAP/+oAD//qAA//6gAP/+oAD//qAA//6gAP/+oAD//qAA//6gAP/+z/z//vUA//6gAP/+oAD//qAA//6gAP/+owD//r8A//6/AP/+0AD//v2C/4L/gv+N//5/AP/+AAD//oCC/4L/hv/+fgD//gAA//4AAP/+jfT//uMA//5zAP/+DgD//gAA//4AAP/+AAD//gAA//4JAP/+YgD//s3o//7AAP/+CwD//gAA//4DAP/+sIL/gv+C/6n//okA//4AAP/+AAD//pPg//6yAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+f/z//uMA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//h4A//67gv+C/4L/kf/+fwD//gAA//6Agv+C/4b//ugA//4IAP/+AAD//hcA//738P/+8QD//o0A//4dAP/+AAD//gAA//4AAP/+AAD//gAA//44AP/+pAD//vnw//7AAP/+DgD//qaC/4L/gv+l//6JAP/+AAD//gAA//6T4P/+sgD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//n/8//7jAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+CQD//tSC/4L/gv+V//5/AP/+AAD//oCC/4L/gv/+YgD//gAA//4AAP/+muj//t8A//4PAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+FgD//nkA//7j9P/+7YL/gv+C/6H//okA//4AAP/+AAD//pPg//6yAP/+AAD//gAA//5dAP/+3wD//t8A//7fAP/+3wD//t8A//7fAP/+7/z//uMA//4AAP/+AAD//jEA//7fAP/+3wD//t8A//60AP/+JgD//gAA//4AAP/+Y4L/gv+C/5X//n8A//4AAP/+gIL/gv+C//7TAP/+AQD//gAA//4nAP/+/vD//uQA//4lAP/+AAD//gAA//5nAP/+TQD//gEA//4AAP/+AAD//gAA//4AAP/+BAD//lEA//6+gv+C/4L/mf/+iQD//gAA//4AAP/+k+D//rIA//4AAP/+AAD//mrg//7jAP/+AAD//gAA//448P/+vgD//gAA//4AAP/+MIL/gv+C/5X//n8A//4AAP/+gIL/gv+C//3//kYA//4AAP/+AAD//rH0//7kAP/+JQD//gAA//4AAP/+efj//tkA//5qAP/+CQD//gAA//4AAP/+AAD//gMA//6mgv+C/4L/mf/+iQD//gAA//4AAP/+k+D//rIA//4AAP/+AAD//mrg//7jAP/+AAD//gAA//448P/+0AD//gAA//4AAP/+MIL/gv+C/5X//n8A//4AAP/+gIL/gv+C//3//rgA//4AAP/+AAD//j34//7kAP/+JQD//gAA//4AAP/+eez//u0A//6GAP/+GQD//gMA//6mgv+C/4L/lf/+iQD//gAA//4AAP/+k+D//rIA//4AAP/+AAD//jUA//6AAP/+gAD//oAA//6AAP/+gAD//rj4//7jAP/+AAD//gAA//449P/+8QD//k0A//4AAP/+AAD//lmC/4L/gv+V//5/AP/+AAD//oCC/4L/gv/9//7+AP/+KwD//gAA//4AAP/+yAD//uQA//4lAP/+AAD//gAA//554P/++QD//tCC/4L/gv+R//6JAP/+AAD//gAA//6T4P/+sgD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+cPj//uMA//4AAP/+AAD//gcA//4gAP/+IAD//iAA//4FAP/+AAD//gAA//4CAP/+w4L/gv+C/5X//n8A//4AAP/+gIL/gv+6//77AP/+rAD//mAA//5AAP/+QAD//mwA//7G3P/+mwD//gAA//4AAP/+RAD//iUA//4AAP/+AAD//nmC/4L/gv+C/+T//okA//4AAP/+AAD//pPg//6yAP/+AAD//gAA//4NAP/+IAD//iAA//4gAP/+IAD//iAA//6C+P/+4wD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+BwD//pmC/4L/gv+R//5/AP/+AAD//oCC/4L/vv/+uAD//iQA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//5RAP/+7uT//vcA//4WAP/+AAD//gAA//4AAP/+AAD//nmC/4L/gv+C/+D//okA//4AAP/+AAD//pPg//6yAP/+AAD//gAA//5q4P/+4wD//gAA//4AAP/+FQD//mAA//5gAP/+YAD//n4A//6WAP/+5IL/gv+C/43//n8A//4AAP/+gIL/gv/C//57AP/+AQD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4OAP/+0uD//n8A//4AAP/+AAD//gAA//55gv+C/4L/gv/c//6JAP/+AAD//gAA//6T4P/+sgD//gAA//4AAP/+auD//uMA//4AAP/+AAD//jiC/4L/gv+C//T//n8A//4AAP/+gIL/gv/G//5oAP/+AAD//gAA//4AAP/+WwD//sIA//7fAP/+yQD//mkA//4HAP/+udz//ukA//4IAP/+AAD//gMA//7igv+C/4L/gv/c//6JAP/+AAD//gAA//6T4P/+sgD//gAA//4AAP/+auD//uMA//4AAP/+AAD//jiC/4L/gv+C//T//n8A//4AAP/+gIL/gv/K//6JAP/+AAD//gAA//4LAP/+suz//unU//5jAP/+AAD//gAA//5wgv+C/4L/gv/c//6JAP/+AAD//gAA//5cAP/+oAD//qAA//6gAP/+oAD//qAA//6gAP/+xfz//rIA//4AAP/+AAD//mrg//7jAP/+AAD//gAA//44gv+C/4L/gv/0//5/AP/+AAD//oCC/4L/zv/+0wD//gUA//4AAP/+CQD//sK4//7UAP/+AQD//gAA//4LAP/+7YL/gv+C/4L/4P/+iQD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//mP8//6yAP/+AAD//gAA//5q4P/+4wD//gAA//4AAP/+OIL/gv+C/4L/9P/+fwD//gAA//6Agv+C/87//k8A//4AAP/+AAD//p3Q//76AP/+9Oj//kcA//4AAP/+AAD//oCC/4L/gv+C/+D//okA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//5j/P/+sgD//gAA//4AAP/+auD//uMA//4AAP/+AAD//jiC/4L/gv+C//T//n8A//4AAP/+gIL/gv/S//7oAP/+AwD//gAA//412P/+0QD//mcA//4eAP/+AAD//gAA//4jAP/+iAD//vn0//64AP/+AAD//gAA//4UAP/+9IL/gv+C/4L/gv+C/4L/gv+C/+f//n8A//4AAP/+gIL/gv/S//6wAP/+AAD//gAA//6G4P/+4AD//lkA//4BAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+NgD//u34//7+AP/+LAD//gAA//4AAP/+kIL/gv+C/4L/gv+C/4L/gv+C/+f//n8A//4AAP/+gIL/gv/S//6hAP/+AAD//gAA//6P6P/++gD//oUA//4JAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+RvT//pwA//4AAP/+AAD//iGC/4L/gv+C/4L/gv+C/4L/gv/n//5/AP/+AAD//oCC/4L/0v/+vwD//gAA//4AAP/+Rez//rsA//4nAP/+AAD//gAA//4AAP/+AAD//kQA//6zAP/+9gD//vEA//6OAP/+BwD//gAA//4AAP/+tfj//vcA//4XAP/+AwD//qaC/4L/gv+C/4L/gv+C/4L/gv/n//5/AP/+AAD//oCC/4L/0v/++AD//hMA//4AAP/+AAD//nMA//72/P/+vQD//kcA//4AAP/+AAD//gAA//4AAP/+KAD//rjs//6TAP/+AAD//gAA//5c9P/+gwD//qaC/4L/gv+C/4L/gv+C/4L/gv/j//5/AP/+AAD//oCC/4L/zv/+jgD//gAA//4AAP/+AAD//gYA//4OAP/+AAD//gAA//4AAP/+AAD//gwA//6IAP/++uj//vYA//4IAP/+AAD//i2C/4L/gv+C/4L/gv+C/4L/gv/P//5/AP/+AAD//oCC/4L/zv/+/gD//mEA//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4CAP/+XwD//uTc//4SAP/+AAD//iaC/4L/gv+C/4L/gv+C/4L/gv/P//5/AP/+AAD//oCC/4L/xv/+oQD//jEA//4AAP/+AAD//h8A//5qAP/+19j//t8A//4BAP/+AAD//kaC/4L/gv+C/4L/gv+C/4L/gv/P//5/AP/+AAD//oCC/4L/gv/+eAD//gAA//4AAP/+ioL/gv+C/4L/gv+C/4L/gv+C/8///n8A//4AAP/+gIL/gv+G//7RAP/+BwD//gAA//4JAP/+6YL/gv+C/4L/gv+C/4L/gv+C/8///n8A//4AAP/+gIL/gv+K//7hAP/+HQD//gAA//4AAP/+gYL/gv+C/4L/gv+C/4L/gv+C/8v//n8A//4AAP/+gIL/gv+q//7m6P/+xQD//hgA//4AAP/+AAD//j0A//77gv+C/4L/gv+C/4L/gv+C/4L/y//+fwD//gAA//6Agv+C/67//rAA//4FAP/+bgD//tz4//7MAP/+WwD//gIA//4AAP/+AAD//jEA//7ugv+C/4L/gv+C/4L/gv+C/4L/x//+fwD//gAA//6Agv+C/7L//tEA//4MAP/+AAD//gAA//4AAP/+GQD//g8A//4AAP/+AAD//gAA//4AAP/+RAD//vCC/4L/gv+C/4L/gv+C/4L/gv/D//5/AP/+AAD//oCC/4L/sv/+8AD//k4A//4AAP/+AAD//gAA//4AAP/+AAD//gAA//4AAP/+BwD//oMA//78gv+C/4L/gv+C/4L/gv+C/4L/v//+fwD//gAA//6Agv+C/6r//rUA//5NAP/+CwD//gAA//4AAP/+JQD//nMA//7hgv+C/4L/gv+C/4L/gv+C/4L/t//+fwD//gAA//6Agv+C/57//umC/4L/gv+C/4L/gv+C/4L/gv+n//5/AP/+AAD//oCC/4L/gv+C/4L/gv+C/4L/gv+C/4L/gv/A//5/AP/+AAD//oCC/4L/gv+C/4L/gv+C/4L/gv+C/4L/gv/A//5/AP/+AAD//oCC/4L/gv+C/4L/gv+C/4L/gv+C/4L/gv/A//5/AP/+AAD//oCC/4L/gv+C/4L/gv+C/4L/gv+C/4L/gv/A//5/AP/+AAD//oCC/4L/gv+C/4L/gv+C/4L/gv+C/4L/gv/A//5/AP/+AAD//oCC/4L/gv+C/4L/gv+C/4L/gv+C/4L/gv/A//5/AP/+AAD//oCC/4L/gv+C/4L/gv+C/4L/gv+C/4L/gv/A//5/AP/+AAD//oCC/4L/gv+C/4L/gv+C/4L/gv+C/4L/gv/A//5/AP/+AAD//oCC/4L/gv+C/4L/gv+C/4L/gv+C/4L/gv/A//5/AP/+AAD//oCC/4L/gv+C/4L/gv+C/4L/gv+C/4L/gv/A//5/AP/+AAD//j8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+fwD//n8A//5/AP/+PwD/AAGe4wAIAAgACAAIAAAAyAAAAAEAAADIAAAAAVVTUFMgQVBJcwAADwEAAAMAAAABAZAAAAEBAAMAAAABASwAAAECAAMAAAAEAAGewQEDAAQAAAABAACABQEGAAMAAAABAAIAAAERAAQAAAABAAAACAESAAMAAAABAAEAAAEVAAMAAAABAAQAAAEWAAMAAAABASwAAAEXAAQAAAABAAGetQEaAAUAAAABAAGeyQEbAAUAAAABAAGe0QEoAAIAAAACMgAAAAExAAIAAAAKAAGe2QFSAAMAAAABAAIAAAAAAAA= +--GB6rO2tUuBXXlNm-LZuY7sA1-- \ No newline at end of file diff --git a/bin/brakeman b/bin/brakeman new file mode 100755 index 0000000..ace1c9b --- /dev/null +++ b/bin/brakeman @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +ARGV.unshift("--ensure-latest") + +load Gem.bin_path("brakeman", "brakeman") diff --git a/bin/bundle b/bin/bundle new file mode 100755 index 0000000..50da5fd --- /dev/null +++ b/bin/bundle @@ -0,0 +1,109 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a.match?(Gem::Version::ANCHORED_VERSION_PATTERN) + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + bundler_version = $1 + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../Gemfile", __dir__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, ".locked") + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_requirement + @bundler_requirement ||= + env_var_version || + cli_arg_version || + bundler_requirement_for(lockfile_version) + end + + def bundler_requirement_for(version) + return "#{Gem::Requirement.default}.a" unless version + + bundler_gem_version = Gem::Version.new(version) + + bundler_gem_version.approximate_recommendation + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem "bundler", bundler_requirement + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("bundler", "bundle") +end diff --git a/bin/dev b/bin/dev new file mode 100755 index 0000000..ad72c7d --- /dev/null +++ b/bin/dev @@ -0,0 +1,16 @@ +#!/usr/bin/env sh + +if ! gem list foreman -i --silent; then + echo "Installing foreman..." + gem install foreman +fi + +# Default to port 3000 if not specified +export PORT="${PORT:-3000}" + +# Let the debug gem allow remote connections, +# but avoid loading until `debugger` is called +export RUBY_DEBUG_OPEN="true" +export RUBY_DEBUG_LAZY="true" + +exec foreman start -f Procfile.dev "$@" diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint new file mode 100755 index 0000000..57567d6 --- /dev/null +++ b/bin/docker-entrypoint @@ -0,0 +1,14 @@ +#!/bin/bash -e + +# Enable jemalloc for reduced memory usage and latency. +if [ -z "${LD_PRELOAD+x}" ]; then + LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit) + export LD_PRELOAD +fi + +# If running the rails server then create or migrate existing database +if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then + ./bin/rails db:prepare +fi + +exec "${@}" diff --git a/bin/jobs b/bin/jobs new file mode 100755 index 0000000..dcf59f3 --- /dev/null +++ b/bin/jobs @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby + +require_relative "../config/environment" +require "solid_queue/cli" + +SolidQueue::Cli.start(ARGV) diff --git a/bin/kamal b/bin/kamal new file mode 100755 index 0000000..cbe59b9 --- /dev/null +++ b/bin/kamal @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'kamal' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("kamal", "kamal") diff --git a/bin/rails b/bin/rails new file mode 100755 index 0000000..efc0377 --- /dev/null +++ b/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/bin/rake b/bin/rake new file mode 100755 index 0000000..4fbf10b --- /dev/null +++ b/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/bin/rubocop b/bin/rubocop new file mode 100755 index 0000000..40330c0 --- /dev/null +++ b/bin/rubocop @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +# explicit rubocop config increases performance slightly while avoiding config confusion. +ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) + +load Gem.bin_path("rubocop", "rubocop") diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..6b62a32 --- /dev/null +++ b/bin/setup @@ -0,0 +1,37 @@ +#!/usr/bin/env ruby +require "fileutils" + +APP_ROOT = File.expand_path("..", __dir__) + +def system!(*args) + system(*args, exception: true) +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts "== Installing dependencies ==" + system("bundle check") || system!("bundle install") + + # Install JavaScript dependencies + system("yarn install --check-files") + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" + # end + + puts "\n== Preparing database ==" + system! "bin/rails db:prepare" + + puts "\n== Removing old logs and tempfiles ==" + system! "bin/rails log:clear tmp:clear" + + unless ARGV.include?("--skip-server") + puts "\n== Starting development server ==" + STDOUT.flush # flush the output before exec(2) so that it displays + exec "bin/dev" + end +end diff --git a/bin/thrust b/bin/thrust new file mode 100755 index 0000000..36bde2d --- /dev/null +++ b/bin/thrust @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("thruster", "thrust") diff --git a/bin/vite b/bin/vite new file mode 100755 index 0000000..5da3388 --- /dev/null +++ b/bin/vite @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'vite' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("vite_ruby", "vite") diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..4a3c09a --- /dev/null +++ b/config.ru @@ -0,0 +1,6 @@ +# This file is used by Rack-based servers to start the application. + +require_relative "config/environment" + +run Rails.application +Rails.application.load_server diff --git a/config/application.rb b/config/application.rb new file mode 100644 index 0000000..e845a03 --- /dev/null +++ b/config/application.rb @@ -0,0 +1,49 @@ +require_relative "boot" + +require "rails" +require "active_model/railtie" +require "active_job/railtie" +require "active_record/railtie" +require "active_storage/engine" +require "action_controller/railtie" +require "action_mailer/railtie" +require "action_mailbox/engine" +require "action_text/engine" +require "action_view/railtie" +require "action_cable/engine" +# require "sprockets/railtie" +require "rails/test_unit/railtie" +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module Theseus + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.assets.enabled = false + config.load_defaults 8.0 + config.active_job.queue_adapter = :good_job + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: %w[assets tasks]) + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + config.generators do |g| + g.template_engine :erb + end + + # Configure session cookie to expire in 30 days for all environments + config.session_store :cookie_store, + key: "_theseus_session", + expire_after: 30.days, + secure: Rails.env.production?, + httponly: true + end +end diff --git a/config/blazer.yml b/config/blazer.yml new file mode 100644 index 0000000..1ad0d7f --- /dev/null +++ b/config/blazer.yml @@ -0,0 +1,79 @@ +# see https://github.com/ankane/blazer for more info + +data_sources: + main: + url: <%= ENV["BLAZER_DATABASE_URL"] %> + + # statement timeout, in seconds + # none by default + # timeout: 15 + + # caching settings + # can greatly improve speed + # off by default + # cache: + # mode: slow # or all + # expires_in: 60 # min + # slow_threshold: 15 # sec, only used in slow mode + + # wrap queries in a transaction for safety + # not necessary if you use a read-only user + # true by default + # use_transaction: false + + smart_variables: + # zone_id: "SELECT id, name FROM zones ORDER BY name ASC" + # period: ["day", "week", "month"] + # status: {0: "Active", 1: "Archived"} + + linked_columns: + # user_id: "/admin/users/{value}" + + smart_columns: + # user_id: "SELECT id, name FROM users WHERE id IN {value}" + +# create audits +audit: true + +# change the time zone +# time_zone: "Pacific Time (US & Canada)" + +# class name of the user model +# user_class: User + +# method name for the current user +# user_method: current_user + +# method name for the display name +# user_name: name + +# custom before_action to use for auth +# before_action_method: require_admin + +# email to send checks from +# from_email: blazer@example.org + +# webhook for Slack +# slack_webhook_url: <%= ENV["BLAZER_SLACK_WEBHOOK_URL"] %> + +check_schedules: + - "1 day" + - "1 hour" + - "5 minutes" + +# enable anomaly detection +# note: with trend, time series are sent to https://trendapi.org +# anomaly_checks: prophet / trend / anomaly_detection + +# enable forecasting +# note: with trend, time series are sent to https://trendapi.org +# forecasting: prophet / trend + +# enable map +# mapbox_access_token: <%= ENV["MAPBOX_ACCESS_TOKEN"] %> + +# enable uploads +# uploads: +# url: <%= ENV["BLAZER_UPLOADS_URL"] %> +# schema: uploads +# data_source: main diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 0000000..988a5dd --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,4 @@ +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 0000000..b9adc5a --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,17 @@ +# Async adapter only works within the same process, so for manually triggering cable updates from a console, +# and seeing results in the browser, you must do so from the web console (running inside the dev process), +# not a terminal started via bin/rails console! Add "console" to any action or any ERB template view +# to make the web console appear. +development: + adapter: async + +test: + adapter: test + +production: + adapter: solid_cable + connects_to: + database: + writing: cable + polling_interval: 0.1.seconds + message_retention: 1.day diff --git a/config/cache.yml b/config/cache.yml new file mode 100644 index 0000000..19d4908 --- /dev/null +++ b/config/cache.yml @@ -0,0 +1,16 @@ +default: &default + store_options: + # Cap age of oldest cache entry to fulfill retention policies + # max_age: <%= 60.days.to_i %> + max_size: <%= 256.megabytes %> + namespace: <%= Rails.env %> + +development: + <<: *default + +test: + <<: *default + +production: + database: cache + <<: *default diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc new file mode 100644 index 0000000..61dc4eb --- /dev/null +++ b/config/credentials.yml.enc @@ -0,0 +1 @@ +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--nG9Of9qoASu0IbS0--qnqgMt3lhJYSG391QOsf+Q== \ No newline at end of file diff --git a/config/credentials/production.yml.enc b/config/credentials/production.yml.enc new file mode 100644 index 0000000..0bbae50 --- /dev/null +++ b/config/credentials/production.yml.enc @@ -0,0 +1 @@ +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--l44W6B8gyy21KSQa--ZpUd1uDcrqlAPWys6aeGTw== \ No newline at end of file diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 0000000..53aa737 --- /dev/null +++ b/config/database.yml @@ -0,0 +1,100 @@ +# PostgreSQL. Versions 9.3 and up are supported. +# +# Install the pg driver: +# gem install pg +# On macOS with Homebrew: +# gem install pg -- --with-pg-config=/usr/local/bin/pg_config +# On Windows: +# gem install pg +# Choose the win32 build. +# Install PostgreSQL and put its /bin directory on your path. +# +# Configure Using Gemfile +# gem "pg" +# +default: &default + adapter: postgresql + encoding: unicode + # For details on connection pooling, see Rails configuration guide + # https://guides.rubyonrails.org/configuring.html#database-pooling + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + url: <%= ENV['DATABASE_URL'] %> + + +development: + <<: *default + database: theseus_development + + + # The specified database role being used to connect to PostgreSQL. + # To create additional roles in PostgreSQL see `$ createuser --help`. + # When left blank, PostgreSQL will use the default role. This is + # the same name as the operating system user running Rails. + #username: theseus + + # The password associated with the PostgreSQL role (username). + #password: + + # Connect on a TCP socket. Omitted by default since the client uses a + # domain socket that doesn't need configuration. Windows does not have + # domain sockets, so uncomment these lines. + #host: localhost + + # The TCP port the server listens on. Defaults to 5432. + # If your server runs on a different port number, change accordingly. + #port: 5432 + + # Schema search path. The server defaults to $user,public + #schema_search_path: myapp,sharedapp,public + + # Minimum log levels, in increasing order: + # debug5, debug4, debug3, debug2, debug1, + # log, notice, warning, error, fatal, and panic + # Defaults to warning. + #min_messages: notice + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: theseus_test + +# As with config/credentials.yml, you never want to store sensitive information, +# like your database password, in your source code. If your source code is +# ever seen by anyone, they now have access to your database. +# +# Instead, provide the password or a full connection URL as an environment +# variable when you boot the app. For example: +# +# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" +# +# If the connection URL is provided in the special DATABASE_URL environment +# variable, Rails will automatically merge its configuration values on top of +# the values provided in this file. Alternatively, you can specify a connection +# URL environment variable explicitly: +# +# production: +# url: <%= ENV["MY_APP_DATABASE_URL"] %> +# +# Read https://guides.rubyonrails.org/configuring.html#configuring-a-database +# for a full overview on how database connection configuration can be specified. +# +production: + primary: &primary_production + <<: *default + database: theseus_production + username: theseus + password: <%= ENV["THESEUS_DATABASE_PASSWORD"] %> + cache: + <<: *primary_production + database: theseus_production_cache + migrations_paths: db/cache_migrate + queue: + <<: *primary_production + database: theseus_production_queue + migrations_paths: db/queue_migrate + cable: + <<: *primary_production + database: theseus_production_cable + migrations_paths: db/cable_migrate diff --git a/config/deploy.yml b/config/deploy.yml new file mode 100644 index 0000000..a7d90cc --- /dev/null +++ b/config/deploy.yml @@ -0,0 +1,116 @@ +# Name of your application. Used to uniquely configure containers. +service: theseus + +# Name of the container image. +image: your-user/theseus + +# Deploy to these servers. +servers: + web: + - 192.168.0.1 + # job: + # hosts: + # - 192.168.0.1 + # cmd: bin/jobs + +# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server. +# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer. +# +# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption. +proxy: + ssl: true + host: app.example.com + +# Credentials for your image host. +registry: + # Specify the registry server, if you're not using Docker Hub + # server: registry.digitalocean.com / ghcr.io / ... + username: your-user + + # Always use an access token rather than real password when possible. + password: + - KAMAL_REGISTRY_PASSWORD + +# Inject ENV variables into containers (secrets come from .kamal/secrets). +env: + secret: + - RAILS_MASTER_KEY + clear: + # Run the Solid Queue Supervisor inside the web server's Puma process to do jobs. + # When you start using multiple servers, you should split out job processing to a dedicated machine. + SOLID_QUEUE_IN_PUMA: true + + # Set number of processes dedicated to Solid Queue (default: 1) + # JOB_CONCURRENCY: 3 + + # Set number of cores available to the application on each server (default: 1). + # WEB_CONCURRENCY: 2 + + # Match this to any external database server to configure Active Record correctly + # Use theseus-db for a db accessory server on same machine via local kamal docker network. + # DB_HOST: 192.168.0.2 + + # Log everything from Rails + # RAILS_LOG_LEVEL: debug + +# Aliases are triggered with "bin/kamal ". You can overwrite arguments on invocation: +# "bin/kamal logs -r job" will tail logs from the first server in the job section. +aliases: + console: app exec --interactive --reuse "bin/rails console" + shell: app exec --interactive --reuse "bash" + logs: app logs -f + dbc: app exec --interactive --reuse "bin/rails dbconsole" + + +# Use a persistent storage volume for sqlite database files and local Active Storage files. +# Recommended to change this to a mounted volume path that is backed up off server. +volumes: + - "theseus_storage:/rails/storage" + + +# Bridge fingerprinted assets, like JS and CSS, between versions to avoid +# hitting 404 on in-flight requests. Combines all files from new and old +# version inside the asset_path. +asset_path: /rails/public/assets + +# Configure the image builder. +builder: + arch: amd64 + + # # Build image via remote server (useful for faster amd64 builds on arm64 computers) + # remote: ssh://docker@docker-builder-server + # + # # Pass arguments and secrets to the Docker build process + # args: + # RUBY_VERSION: 3.3.6 + # secrets: + # - GITHUB_TOKEN + # - RAILS_MASTER_KEY + +# Use a different ssh user than root +# ssh: +# user: app + +# Use accessory services (secrets come from .kamal/secrets). +# accessories: +# db: +# image: mysql:8.0 +# host: 192.168.0.2 +# # Change to 3306 to expose port to the world instead of just local network. +# port: "127.0.0.1:3306:3306" +# env: +# clear: +# MYSQL_ROOT_HOST: '%' +# secret: +# - MYSQL_ROOT_PASSWORD +# files: +# - config/mysql/production.cnf:/etc/mysql/my.cnf +# - db/production.sql:/docker-entrypoint-initdb.d/setup.sql +# directories: +# - data:/var/lib/mysql +# redis: +# image: redis:7.0 +# host: 192.168.0.2 +# port: 6379 +# directories: +# - data:/data diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 0000000..cac5315 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 0000000..799d4ad --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,77 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Make code changes take effect immediately without server restart. + config.enable_reloading = true + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable server timing. + config.server_timing = true + + # Enable/disable Action Controller caching. By default Action Controller caching is disabled. + # Run rails dev:cache to toggle Action Controller caching. + if Rails.root.join("tmp/caching-dev.txt").exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" } + else + config.action_controller.perform_caching = false + end + + # Change to :null_store to avoid any caching. + config.cache_store = :memory_store + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + # Make template changes take effect immediately. + config.action_mailer.perform_caching = false + + # Set localhost to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "localhost", port: 3000 } + + config.action_mailer.delivery_method = :letter_opener_web + + config.action_mailer.perform_deliveries = true + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Append comments with runtime information tags to SQL queries in logs. + config.active_record.query_log_tags_enabled = true + + # Highlight code that enqueued background job in logs. + config.active_job.verbose_enqueue_logs = true + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + config.action_view.annotate_rendered_view_with_filenames = true + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true + + # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. + # config.generators.apply_rubocop_autocorrect_after_generate! + config.hosts.clear +end diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 0000000..70cfebe --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,90 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.enable_reloading = false + + # Eager load code on boot for better performance and memory savings (ignored by Rake tasks). + config.eager_load = true + + # Full error reports are disabled. + config.consider_all_requests_local = false + + # Turn on fragment caching in view templates. + config.action_controller.perform_caching = true + + # Cache assets for far-future expiry since they are all digest stamped. + config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" } + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :hetzner + + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + config.assume_ssl = true + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + config.force_ssl = true + + # Skip http-to-https redirect for the default health check endpoint. + # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } + + # Log to STDOUT with the current request id as a default log tag. + config.log_tags = [ :request_id ] + config.logger = ActiveSupport::TaggedLogging.logger(STDOUT) + + # Change to "debug" to log everything (including potentially personally-identifiable information!) + config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") + + # Prevent health checks from clogging up the logs. + config.silence_healthcheck_path = "/up" + + # Don't log any deprecations. + config.active_support.report_deprecations = false + + # Replace the default in-process memory cache store with a durable alternative. + config.cache_store = :redis_cache_store, { url: ENV["REDIS_URL"] } + + # Replace the default in-process and non-durable queuing backend for Active Job. + config.active_job.queue_adapter = :good_job + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Set host to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "mail.hackclub.com" } + + config.action_mailer.delivery_method = :smtp + config.action_mailer.smtp_settings = { + address: "smtp.loops.so", + port: 587, + user_name: "loops", + password: Rails.application.credentials.dig(:loops, :api_key), + authentication: "plain", + enable_starttls: true + } + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Only use :id for inspections in production. + config.active_record.attributes_for_inspect = [ :id ] + + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + # + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } +end diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 0000000..c2095b1 --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,53 @@ +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # While tests run files are not watched, reloading is not necessary. + config.enable_reloading = false + + # Eager loading loads your entire application. When running a single test locally, + # this is usually not necessary, and can slow down your test suite. However, it's + # recommended that you enable it in continuous integration systems to ensure eager + # loading is working properly before deploying your code. + config.eager_load = ENV["CI"].present? + + # Configure public file server for tests with cache-control for performance. + config.public_file_server.headers = { "cache-control" => "public, max-age=3600" } + + # Show full error reports. + config.consider_all_requests_local = true + config.cache_store = :null_store + + # Render exception templates for rescuable exceptions and raise for other exceptions. + config.action_dispatch.show_exceptions = :rescuable + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Store uploaded files on the local file system in a temporary directory. + config.active_storage.service = :test + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Set host to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "example.com" } + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/config/honeybadger.yml b/config/honeybadger.yml new file mode 100644 index 0000000..f0feaec --- /dev/null +++ b/config/honeybadger.yml @@ -0,0 +1,34 @@ +--- +# For more options, see https://docs.honeybadger.io/lib/ruby/gem-reference/configuration + +api_key: '<%= ENV["HONEYBADGER_API_KEY"] %>' + +# The environment your app is running in. +env: "<%= Rails.env %>" + +# The absolute path to your project folder. +root: "<%= Rails.root.to_s %>" + +# Honeybadger won't report errors in these environments. +development_environments: +- test +- development +- cucumber + +# By default, Honeybadger won't report errors in the development_environments. +# You can override this by explicitly setting report_data to true or false. +# report_data: true + +# The current Git revision of your project. Defaults to the last commit hash. +# revision: null + +# Enable verbose debug logging (useful for troubleshooting). +debug: false + +# Enable Honeybadger Insights +insights: + enabled: false + +user_informer: + enabled: true + info: "{{error_id}}" \ No newline at end of file diff --git a/config/initializers/awesome_print.rb b/config/initializers/awesome_print.rb new file mode 100644 index 0000000..de45c90 --- /dev/null +++ b/config/initializers/awesome_print.rb @@ -0,0 +1,8 @@ +AwesomePrint.defaults = { + color: { + integer: :cyan, + float: :cyan, + bigdecimal: :cyan, + rational: :cyan # bleh + } +} diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb new file mode 100644 index 0000000..aa21c5f --- /dev/null +++ b/config/initializers/content_security_policy.rb @@ -0,0 +1,34 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header + +# Rails.application.configure do +# config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https + # Allow @vite/client to hot reload javascript changes in development +# policy.script_src *policy.script_src, :unsafe_eval, "http://#{ ViteRuby.config.host_with_port }" if Rails.env.development? + + # You may need to enable this in production as well depending on your setup. +# policy.script_src *policy.script_src, :blob if Rails.env.test? + +# policy.style_src :self, :https + # Allow @vite/client to hot reload style changes in development +# policy.style_src *policy.style_src, :unsafe_inline if Rails.env.development? + +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end +# +# # Generate session nonces for permitted importmap, inline scripts, and inline styles. +# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } +# config.content_security_policy_nonce_directives = %w(script-src style-src) +# +# # Report violations without enforcing the policy. +# # config.content_security_policy_report_only = true +# end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000..c0b717f --- /dev/null +++ b/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. +# Use this to limit dissemination of sensitive information. +# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. +Rails.application.config.filter_parameters += [ + :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc +] diff --git a/config/initializers/git_version.rb b/config/initializers/git_version.rb new file mode 100644 index 0000000..0e7b036 --- /dev/null +++ b/config/initializers/git_version.rb @@ -0,0 +1,22 @@ +# Get the first 6 characters of the current git commit hash +git_hash = ENV["SOURCE_COMMIT"] || `git rev-parse HEAD` rescue "unknown" + +commit_link = git_hash != "unknown" ? "https://github.com/hackclub/harbor/commit/#{git_hash}" : nil + +short_hash = git_hash[0..7] + +commit_count = `git rev-list --count HEAD`.strip rescue 0 + +# Check if there are any uncommitted changes +is_dirty = `git status --porcelain`.strip.length > 0 rescue false + +# Append "-dirty" if there are uncommitted changes +version = is_dirty ? "#{short_hash}-dirty" : short_hash + +# Store server start time +Rails.application.config.server_start_time = Time.current + +# Store the version +Rails.application.config.git_version = version +Rails.application.config.git_commit_count = commit_count +Rails.application.config.commit_link = commit_link diff --git a/config/initializers/good_job.rb b/config/initializers/good_job.rb new file mode 100644 index 0000000..1d7a8a1 --- /dev/null +++ b/config/initializers/good_job.rb @@ -0,0 +1,44 @@ +Rails.application.configure do + config.good_job.preserve_job_records = true + config.good_job.enable_cron = Rails.env.production? + config.good_job.execution_mode = :async + + config.good_job.cron = { + update_mailing_info: { + cron: "*/5 * * * *", + class: "Warehouse::UpdateMailingInfoJob", + }, + update_median_postage_costs: { + cron: "*/30 * * * *", + class: "Warehouse::UpdateMedianPostageCostsJob", + }, + update_inventory_levels: { + cron: "*/5 * * * *", + class: "Warehouse::UpdateInventoryLevelsJob", + }, + update_cancellations: { + cron: "*/10 * * * *", + class: "Warehouse::UpdateCancellationsJob", + }, + update_map_data: { + cron: "*/30 * * * *", + class: "Public::UpdateMapDataJob", + }, + sync_skus: { + cron: "*/7 * * * *", + class: "TableSync::SKUSyncJob", + }, + sync_orders: { + cron: "*/8 * * * *", + class: "TableSync::OrderSyncJob", + }, + usps_pocketwatch: { + cron: "0 5 * * *", # 5:00 UTC = midnight EST + class: "USPS::PaymentAccount::PocketWatchJob", + }, + airtable_athena_stickers_etl: { + cron: "*/22 * * * *", + class: "AirtableETL::AthenaStickersETLJob", + }, + } +end diff --git a/config/initializers/hashids.rb b/config/initializers/hashids.rb new file mode 100644 index 0000000..5d0ee19 --- /dev/null +++ b/config/initializers/hashids.rb @@ -0,0 +1,19 @@ +Hashid::Rails.configure do |config| + # The salt to use for generating hashid. Prepended with pepper (table name). + config.salt = Rails.application.credentials[:hashid_salt] + + # The minimum length of generated hashids + config.min_hash_length = 6 + + # The alphabet to use for generating hashids + config.alphabet = "cdefhjkmnprtvwxy2345689" + + # Whether to override the `find` method + config.override_find = true + + # Whether to override the `to_param` method + config.override_to_param = true + + # Whether to sign hashids to prevent conflicts with regular IDs (see https://github.com/jcypret/hashid-rails/issues/30) + config.sign_hashids = true +end diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb new file mode 100644 index 0000000..ed73d69 --- /dev/null +++ b/config/initializers/inflections.rb @@ -0,0 +1,39 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, "\\1en" +# inflect.singular /^(ox)en/i, "\\1" +# inflect.irregular "person", "people" +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +ActiveSupport::Inflector.inflections(:en) do |inflect| + inflect.acronym "USPS" + inflect.acronym "API" + inflect.irregular "indicium", "indicia" + inflect.acronym "EasyPost" + inflect.acronym "SKU" + inflect.acronym "SKUs" + inflect.acronym "IMb" + inflect.acronym "QR" + inflect.acronym "HCB" + inflect.acronym "IV" + inflect.acronym "MTR" + inflect.acronym "JSON" + inflect.acronym "FLIRT" + inflect.acronym "IMI" + inflect.acronym "FIM" + inflect.acronym "EPS" + inflect.irregular "is", "are" + inflect.irregular "this", "these" + inflect.acronym "AI" + inflect.acronym "LSV" + inflect.acronym "MSR" + inflect.acronym "QZ" + inflect.acronym "OTP" + inflect.acronym "ETL" +end diff --git a/config/initializers/monkey_patches.rb b/config/initializers/monkey_patches.rb new file mode 100644 index 0000000..e69de29 diff --git a/config/initializers/norairrecord.rb b/config/initializers/norairrecord.rb new file mode 100644 index 0000000..5d69fcd --- /dev/null +++ b/config/initializers/norairrecord.rb @@ -0,0 +1,3 @@ +Norairrecord.api_key = Rails.application.credentials.dig(:airtable, :pat) +Norairrecord.user_agent = "Theseus (#{Rails.env})" +Norairrecord.base_url = ENV["AIRTABLE_BASE_URL"] diff --git a/config/initializers/openai.rb b/config/initializers/openai.rb new file mode 100644 index 0000000..18bb12f --- /dev/null +++ b/config/initializers/openai.rb @@ -0,0 +1,4 @@ +OpenAI.configure do |config| + config.access_token = Rails.application.credentials.dig(:openai, :access_token) + config.log_errors = true +end diff --git a/config/initializers/phlex.rb b/config/initializers/phlex.rb new file mode 100644 index 0000000..44b6e85 --- /dev/null +++ b/config/initializers/phlex.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Views +end + +module Components + extend Phlex::Kit +end + +Rails.autoloaders.main.push_dir( + Rails.root.join("app/views"), namespace: Views +) + +Rails.autoloaders.main.push_dir( + Rails.root.join("app/components"), namespace: Components +) diff --git a/config/initializers/snail_mail.rb b/config/initializers/snail_mail.rb new file mode 100644 index 0000000..014133c --- /dev/null +++ b/config/initializers/snail_mail.rb @@ -0,0 +1,4 @@ +# Initializer to load SnailMail templates +Rails.application.config.after_initialize do + SnailMail::Templates.available_templates +end diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 0000000..a986337 --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,54 @@ +# Files in the config/locales directory are used for internationalization and +# are automatically loaded by Rails. If you want to use locales other than +# English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more about the API, please read the Rails Internationalization guide +# at https://guides.rubyonrails.org/i18n.html. +# +# Be aware that YAML interprets the following case-insensitive strings as +# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings +# must be quoted to be interpreted as strings. For example: +# +# en: +# "yes": yup +# enabled: "ON" + +en: + activerecord: + attributes: + user: + home_mid: "Home Mailer ID" + home_return_address: "Home Return Address" + usps_payment_account: + ach: "is ACH?" + helpers: + label: + user: + home_mid_id: "Home Mailer ID" + home_return_address_id: "Home Return Address" + slack_id: "Slack ID" + icon_url: "Avatar URL" + can_warehouse: "Can use warehouse?" + is_admin: "Is admin?" + can_impersonate_public: "Can impersonate public users?" + warehouse_sku: + sku: "SKU" + zenventory_id: "Zenventory internal ID" + usps_payment_account: + ach: "is ACH?" + +# TODO: localize models so stuff is more obvious diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 0000000..a248513 --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,41 @@ +# This configuration file will be evaluated by Puma. The top-level methods that +# are invoked here are part of Puma's configuration DSL. For more information +# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. +# +# Puma starts a configurable number of processes (workers) and each process +# serves each request in a thread from an internal thread pool. +# +# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You +# should only set this value when you want to run 2 or more workers. The +# default is already 1. +# +# The ideal number of threads per worker depends both on how much time the +# application spends waiting for IO operations and on how much you wish to +# prioritize throughput over latency. +# +# As a rule of thumb, increasing the number of threads will increase how much +# traffic a given process can handle (throughput), but due to CRuby's +# Global VM Lock (GVL) it has diminishing returns and will degrade the +# response time (latency) of the application. +# +# The default is set to 3 threads as it's deemed a decent compromise between +# throughput and latency for the average Rails application. +# +# Any libraries that use a connection pool or another resource pool should +# be configured to provide at least as many connections as the number of +# threads. This includes Active Record's `pool` parameter in `database.yml`. +threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +port ENV.fetch("PORT", 3000) + +# Allow puma to be restarted by `bin/rails restart` command. +plugin :tmp_restart + +# Run the Solid Queue supervisor inside of Puma for single-server deployments +plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] + +# Specify the PID file. Defaults to tmp/pids/server.pid in development. +# In other environments, only set the PID file if requested. +pidfile ENV["PIDFILE"] if ENV["PIDFILE"] diff --git a/config/queue.yml b/config/queue.yml new file mode 100644 index 0000000..9eace59 --- /dev/null +++ b/config/queue.yml @@ -0,0 +1,18 @@ +default: &default + dispatchers: + - polling_interval: 1 + batch_size: 500 + workers: + - queues: "*" + threads: 3 + processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %> + polling_interval: 0.1 + +development: + <<: *default + +test: + <<: *default + +production: + <<: *default diff --git a/config/recurring.yml b/config/recurring.yml new file mode 100644 index 0000000..d045b19 --- /dev/null +++ b/config/recurring.yml @@ -0,0 +1,10 @@ +# production: +# periodic_cleanup: +# class: CleanSoftDeletedRecordsJob +# queue: background +# args: [ 1000, { batch_size: 500 } ] +# schedule: every hour +# periodic_command: +# command: "SoftDeletedRecord.due.delete_all" +# priority: 2 +# schedule: at 5am every day diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000..ff28cd2 --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,698 @@ +# == Route Map +# +# Prefix Verb URI Pattern Controller#Action +# lookup_public_ids POST /back_office/public_ids/lookup(.:format) public_ids#lookup +# public_ids GET /back_office/public_ids(.:format) public_ids#index +# inspect_iv_mtr_event GET /back_office/inspect/iv_mtr_events/:id(.:format) inspect/iv_mtr_events#show +# inspect_indicium GET /back_office/inspect/indicia/:id(.:format) inspect/indicia#show +# badge_tasks GET /back_office/my/tasks/badge(.:format) tasks#badge +# refresh_tasks POST /back_office/my/tasks/refresh(.:format) tasks#refresh +# tasks GET /back_office/my/tasks(.:format) tasks#show +# tags GET /back_office/tags(.:format) tags#index +# tag_stats GET /back_office/tags/:id(.:format) tags#show +# refresh_tags POST /back_office/tags/refresh(.:format) tags#refresh +# generate_label_letter POST /back_office/letters/:id/generate_label(.:format) letters#generate_label +# buy_indicia_letter POST /back_office/letters/:id/buy_indicia(.:format) letters#buy_indicia +# mark_printed_letter POST /back_office/letters/:id/mark_printed(.:format) letters#mark_printed +# mark_mailed_letter POST /back_office/letters/:id/mark_mailed(.:format) letters#mark_mailed +# mark_received_letter POST /back_office/letters/:id/mark_received(.:format) letters#mark_received +# clear_label_letter POST /back_office/letters/:id/clear_label(.:format) letters#clear_label +# preview_template_letter GET /back_office/letters/:id/preview_template(.:format) letters#preview_template +# letters GET /back_office/letters(.:format) letters#index +# POST /back_office/letters(.:format) letters#create +# new_letter GET /back_office/letters/new(.:format) letters#new +# edit_letter GET /back_office/letters/:id/edit(.:format) letters#edit +# letter GET /back_office/letters/:id(.:format) letters#show +# PATCH /back_office/letters/:id(.:format) letters#update +# PUT /back_office/letters/:id(.:format) letters#update +# DELETE /back_office/letters/:id(.:format) letters#destroy +# map_fields_letter_batch GET /back_office/letter/batches/:id/map(.:format) letter/batches#map_fields +# set_mapping_letter_batch POST /back_office/letter/batches/:id/set_mapping(.:format) letter/batches#set_mapping +# process_confirm_letter_batch GET /back_office/letter/batches/:id/process(.:format) letter/batches#process_form +# process_letter_batch POST /back_office/letter/batches/:id/process(.:format) letter/batches#process_batch +# mark_printed_letter_batch POST /back_office/letter/batches/:id/mark_printed(.:format) letter/batches#mark_printed +# mark_mailed_letter_batch POST /back_office/letter/batches/:id/mark_mailed(.:format) letter/batches#mark_mailed +# update_costs_letter_batch POST /back_office/letter/batches/:id/update_costs(.:format) letter/batches#update_costs +# regenerate_form_letter_batch GET /back_office/letter/batches/:id/regen(.:format) letter/batches#regenerate_form +# regenerate_labels_letter_batch POST /back_office/letter/batches/:id/regen(.:format) letter/batches#regenerate_labels +# letter_batches GET /back_office/letter/batches(.:format) letter/batches#index +# POST /back_office/letter/batches(.:format) letter/batches#create +# new_letter_batch GET /back_office/letter/batches/new(.:format) letter/batches#new +# edit_letter_batch GET /back_office/letter/batches/:id/edit(.:format) letter/batches#edit +# letter_batch GET /back_office/letter/batches/:id(.:format) letter/batches#show +# PATCH /back_office/letter/batches/:id(.:format) letter/batches#update +# PUT /back_office/letter/batches/:id(.:format) letter/batches#update +# DELETE /back_office/letter/batches/:id(.:format) letter/batches#destroy +# make_batch_from_letter_queue POST /back_office/letter/queues/:id/batch(.:format) letter/queues#batch +# letter_queues GET /back_office/letter/queues(.:format) letter/queues#index +# POST /back_office/letter/queues(.:format) letter/queues#create +# new_letter_queue GET /back_office/letter/queues/new(.:format) letter/queues#new +# edit_letter_queue GET /back_office/letter/queues/:id/edit(.:format) letter/queues#edit +# letter_queue GET /back_office/letter/queues/:id(.:format) letter/queues#show +# PATCH /back_office/letter/queues/:id(.:format) letter/queues#update +# PUT /back_office/letter/queues/:id(.:format) letter/queues#update +# DELETE /back_office/letter/queues/:id(.:format) letter/queues#destroy +# letter_instant_queues GET /back_office/letter/instant_queues(.:format) letter/instant_queues#index +# POST /back_office/letter/instant_queues(.:format) letter/instant_queues#create +# new_letter_instant_queue GET /back_office/letter/instant_queues/new(.:format) letter/instant_queues#new +# edit_letter_instant_queue GET /back_office/letter/instant_queues/:id/edit(.:format) letter/instant_queues#edit +# letter_instant_queue GET /back_office/letter/instant_queues/:id(.:format) letter/instant_queues#show +# PATCH /back_office/letter/instant_queues/:id(.:format) letter/instant_queues#update +# PUT /back_office/letter/instant_queues/:id(.:format) letter/instant_queues#update +# DELETE /back_office/letter/instant_queues/:id(.:format) letter/instant_queues#destroy +# revoke_confirm_api_key GET /back_office/api_keys/:id/revoke(.:format) api_keys#revoke_confirm +# revoke_api_key POST /back_office/api_keys/:id/revoke(.:format) api_keys#revoke +# api_keys GET /back_office/api_keys(.:format) api_keys#index +# POST /back_office/api_keys(.:format) api_keys#create +# new_api_key GET /back_office/api_keys/new(.:format) api_keys#new +# edit_api_key GET /back_office/api_keys/:id/edit(.:format) api_keys#edit +# api_key GET /back_office/api_keys/:id(.:format) api_keys#show +# PATCH /back_office/api_keys/:id(.:format) api_keys#update +# PUT /back_office/api_keys/:id(.:format) api_keys#update +# DELETE /back_office/api_keys/:id(.:format) api_keys#destroy +# admin_addresses GET /back_office/admin/addresses(.:format) admin/addresses#index +# POST /back_office/admin/addresses(.:format) admin/addresses#create +# new_admin_address GET /back_office/admin/addresses/new(.:format) admin/addresses#new +# edit_admin_address GET /back_office/admin/addresses/:id/edit(.:format) admin/addresses#edit +# admin_address GET /back_office/admin/addresses/:id(.:format) admin/addresses#show +# PATCH /back_office/admin/addresses/:id(.:format) admin/addresses#update +# PUT /back_office/admin/addresses/:id(.:format) admin/addresses#update +# DELETE /back_office/admin/addresses/:id(.:format) admin/addresses#destroy +# admin_return_addresses GET /back_office/admin/return_addresses(.:format) admin/return_addresses#index +# POST /back_office/admin/return_addresses(.:format) admin/return_addresses#create +# new_admin_return_address GET /back_office/admin/return_addresses/new(.:format) admin/return_addresses#new +# edit_admin_return_address GET /back_office/admin/return_addresses/:id/edit(.:format) admin/return_addresses#edit +# admin_return_address GET /back_office/admin/return_addresses/:id(.:format) admin/return_addresses#show +# PATCH /back_office/admin/return_addresses/:id(.:format) admin/return_addresses#update +# PUT /back_office/admin/return_addresses/:id(.:format) admin/return_addresses#update +# DELETE /back_office/admin/return_addresses/:id(.:format) admin/return_addresses#destroy +# admin_source_tags GET /back_office/admin/source_tags(.:format) admin/source_tags#index +# POST /back_office/admin/source_tags(.:format) admin/source_tags#create +# new_admin_source_tag GET /back_office/admin/source_tags/new(.:format) admin/source_tags#new +# edit_admin_source_tag GET /back_office/admin/source_tags/:id/edit(.:format) admin/source_tags#edit +# admin_source_tag GET /back_office/admin/source_tags/:id(.:format) admin/source_tags#show +# PATCH /back_office/admin/source_tags/:id(.:format) admin/source_tags#update +# PUT /back_office/admin/source_tags/:id(.:format) admin/source_tags#update +# DELETE /back_office/admin/source_tags/:id(.:format) admin/source_tags#destroy +# admin_users GET /back_office/admin/users(.:format) admin/users#index +# POST /back_office/admin/users(.:format) admin/users#create +# new_admin_user GET /back_office/admin/users/new(.:format) admin/users#new +# edit_admin_user GET /back_office/admin/users/:id/edit(.:format) admin/users#edit +# admin_user GET /back_office/admin/users/:id(.:format) admin/users#show +# PATCH /back_office/admin/users/:id(.:format) admin/users#update +# PUT /back_office/admin/users/:id(.:format) admin/users#update +# DELETE /back_office/admin/users/:id(.:format) admin/users#destroy +# admin_warehouse_templates GET /back_office/admin/warehouse/templates(.:format) admin/warehouse/templates#index +# POST /back_office/admin/warehouse/templates(.:format) admin/warehouse/templates#create +# new_admin_warehouse_template GET /back_office/admin/warehouse/templates/new(.:format) admin/warehouse/templates#new +# edit_admin_warehouse_template GET /back_office/admin/warehouse/templates/:id/edit(.:format) admin/warehouse/templates#edit +# admin_warehouse_template GET /back_office/admin/warehouse/templates/:id(.:format) admin/warehouse/templates#show +# PATCH /back_office/admin/warehouse/templates/:id(.:format) admin/warehouse/templates#update +# PUT /back_office/admin/warehouse/templates/:id(.:format) admin/warehouse/templates#update +# DELETE /back_office/admin/warehouse/templates/:id(.:format) admin/warehouse/templates#destroy +# admin_warehouse_orders GET /back_office/admin/warehouse/orders(.:format) admin/warehouse/orders#index +# POST /back_office/admin/warehouse/orders(.:format) admin/warehouse/orders#create +# new_admin_warehouse_order GET /back_office/admin/warehouse/orders/new(.:format) admin/warehouse/orders#new +# edit_admin_warehouse_order GET /back_office/admin/warehouse/orders/:id/edit(.:format) admin/warehouse/orders#edit +# admin_warehouse_order GET /back_office/admin/warehouse/orders/:id(.:format) admin/warehouse/orders#show +# PATCH /back_office/admin/warehouse/orders/:id(.:format) admin/warehouse/orders#update +# PUT /back_office/admin/warehouse/orders/:id(.:format) admin/warehouse/orders#update +# DELETE /back_office/admin/warehouse/orders/:id(.:format) admin/warehouse/orders#destroy +# admin_warehouse_skus GET /back_office/admin/warehouse/skus(.:format) admin/warehouse/skus#index +# POST /back_office/admin/warehouse/skus(.:format) admin/warehouse/skus#create +# new_admin_warehouse_sku GET /back_office/admin/warehouse/skus/new(.:format) admin/warehouse/skus#new +# edit_admin_warehouse_sku GET /back_office/admin/warehouse/skus/:id/edit(.:format) admin/warehouse/skus#edit +# admin_warehouse_sku GET /back_office/admin/warehouse/skus/:id(.:format) admin/warehouse/skus#show +# PATCH /back_office/admin/warehouse/skus/:id(.:format) admin/warehouse/skus#update +# PUT /back_office/admin/warehouse/skus/:id(.:format) admin/warehouse/skus#update +# DELETE /back_office/admin/warehouse/skus/:id(.:format) admin/warehouse/skus#destroy +# admin_usps_mailer_ids GET /back_office/admin/usps/mailer_ids(.:format) admin/usps/mailer_ids#index +# POST /back_office/admin/usps/mailer_ids(.:format) admin/usps/mailer_ids#create +# new_admin_usps_mailer_id GET /back_office/admin/usps/mailer_ids/new(.:format) admin/usps/mailer_ids#new +# edit_admin_usps_mailer_id GET /back_office/admin/usps/mailer_ids/:id/edit(.:format) admin/usps/mailer_ids#edit +# admin_usps_mailer_id GET /back_office/admin/usps/mailer_ids/:id(.:format) admin/usps/mailer_ids#show +# PATCH /back_office/admin/usps/mailer_ids/:id(.:format) admin/usps/mailer_ids#update +# PUT /back_office/admin/usps/mailer_ids/:id(.:format) admin/usps/mailer_ids#update +# DELETE /back_office/admin/usps/mailer_ids/:id(.:format) admin/usps/mailer_ids#destroy +# admin_usps_payment_accounts GET /back_office/admin/usps/payment_accounts(.:format) admin/usps/payment_accounts#index +# POST /back_office/admin/usps/payment_accounts(.:format) admin/usps/payment_accounts#create +# new_admin_usps_payment_account GET /back_office/admin/usps/payment_accounts/new(.:format) admin/usps/payment_accounts#new +# edit_admin_usps_payment_account GET /back_office/admin/usps/payment_accounts/:id/edit(.:format) admin/usps/payment_accounts#edit +# admin_usps_payment_account GET /back_office/admin/usps/payment_accounts/:id(.:format) admin/usps/payment_accounts#show +# PATCH /back_office/admin/usps/payment_accounts/:id(.:format) admin/usps/payment_accounts#update +# PUT /back_office/admin/usps/payment_accounts/:id(.:format) admin/usps/payment_accounts#update +# DELETE /back_office/admin/usps/payment_accounts/:id(.:format) admin/usps/payment_accounts#destroy +# admin_common_tags GET /back_office/admin/common_tags(.:format) admin/common_tags#index +# POST /back_office/admin/common_tags(.:format) admin/common_tags#create +# new_admin_common_tag GET /back_office/admin/common_tags/new(.:format) admin/common_tags#new +# edit_admin_common_tag GET /back_office/admin/common_tags/:id/edit(.:format) admin/common_tags#edit +# admin_common_tag GET /back_office/admin/common_tags/:id(.:format) admin/common_tags#show +# PATCH /back_office/admin/common_tags/:id(.:format) admin/common_tags#update +# PUT /back_office/admin/common_tags/:id(.:format) admin/common_tags#update +# DELETE /back_office/admin/common_tags/:id(.:format) admin/common_tags#destroy +# admin_root GET /back_office/admin(.:format) admin/users#index +# good_job /back_office/good_job GoodJob::Engine +# blazer /back_office/blazer Blazer::Engine +# impersonate_user GET /back_office/impersonate/:id(.:format) sessions#impersonate +# stop_impersonating GET /back_office/stop_impersonating(.:format) sessions#stop_impersonating +# usps_indicia GET /back_office/usps/indicia(.:format) usps/indicia#index +# POST /back_office/usps/indicia(.:format) usps/indicia#create +# new_usps_indicium GET /back_office/usps/indicia/new(.:format) usps/indicia#new +# edit_usps_indicium GET /back_office/usps/indicia/:id/edit(.:format) usps/indicia#edit +# usps_indicium GET /back_office/usps/indicia/:id(.:format) usps/indicia#show +# PATCH /back_office/usps/indicia/:id(.:format) usps/indicia#update +# PUT /back_office/usps/indicia/:id(.:format) usps/indicia#update +# DELETE /back_office/usps/indicia/:id(.:format) usps/indicia#destroy +# usps_payment_accounts GET /back_office/usps/payment_accounts(.:format) usps/payment_accounts#index +# POST /back_office/usps/payment_accounts(.:format) usps/payment_accounts#create +# new_usps_payment_account GET /back_office/usps/payment_accounts/new(.:format) usps/payment_accounts#new +# edit_usps_payment_account GET /back_office/usps/payment_accounts/:id/edit(.:format) usps/payment_accounts#edit +# usps_payment_account GET /back_office/usps/payment_accounts/:id(.:format) usps/payment_accounts#show +# PATCH /back_office/usps/payment_accounts/:id(.:format) usps/payment_accounts#update +# PUT /back_office/usps/payment_accounts/:id(.:format) usps/payment_accounts#update +# DELETE /back_office/usps/payment_accounts/:id(.:format) usps/payment_accounts#destroy +# usps_mailer_ids GET /back_office/usps/mailer_ids(.:format) usps/mailer_ids#index +# POST /back_office/usps/mailer_ids(.:format) usps/mailer_ids#create +# new_usps_mailer_id GET /back_office/usps/mailer_ids/new(.:format) usps/mailer_ids#new +# edit_usps_mailer_id GET /back_office/usps/mailer_ids/:id/edit(.:format) usps/mailer_ids#edit +# usps_mailer_id GET /back_office/usps/mailer_ids/:id(.:format) usps/mailer_ids#show +# PATCH /back_office/usps/mailer_ids/:id(.:format) usps/mailer_ids#update +# PUT /back_office/usps/mailer_ids/:id(.:format) usps/mailer_ids#update +# DELETE /back_office/usps/mailer_ids/:id(.:format) usps/mailer_ids#destroy +# source_tags GET /back_office/source_tags(.:format) source_tags#index +# POST /back_office/source_tags(.:format) source_tags#create +# new_source_tag GET /back_office/source_tags/new(.:format) source_tags#new +# edit_source_tag GET /back_office/source_tags/:id/edit(.:format) source_tags#edit +# source_tag GET /back_office/source_tags/:id(.:format) source_tags#show +# PATCH /back_office/source_tags/:id(.:format) source_tags#update +# PUT /back_office/source_tags/:id(.:format) source_tags#update +# DELETE /back_office/source_tags/:id(.:format) source_tags#destroy +# warehouse_templates GET /back_office/warehouse/templates(.:format) warehouse/templates#index +# POST /back_office/warehouse/templates(.:format) warehouse/templates#create +# new_warehouse_template GET /back_office/warehouse/templates/new(.:format) warehouse/templates#new +# edit_warehouse_template GET /back_office/warehouse/templates/:id/edit(.:format) warehouse/templates#edit +# warehouse_template GET /back_office/warehouse/templates/:id(.:format) warehouse/templates#show +# PATCH /back_office/warehouse/templates/:id(.:format) warehouse/templates#update +# PUT /back_office/warehouse/templates/:id(.:format) warehouse/templates#update +# DELETE /back_office/warehouse/templates/:id(.:format) warehouse/templates#destroy +# cancel_warehouse_order GET /back_office/warehouse/orders/:id/cancel(.:format) warehouse/orders#cancel +# POST /back_office/warehouse/orders/:id/cancel(.:format) warehouse/orders#confirm_cancel +# send_to_warehouse_warehouse_order POST /back_office/warehouse/orders/:id/send_to_warehouse(.:format) warehouse/orders#send_to_warehouse +# warehouse_orders GET /back_office/warehouse/orders(.:format) warehouse/orders#index +# POST /back_office/warehouse/orders(.:format) warehouse/orders#create +# new_warehouse_order GET /back_office/warehouse/orders/new(.:format) warehouse/orders#new +# edit_warehouse_order GET /back_office/warehouse/orders/:id/edit(.:format) warehouse/orders#edit +# warehouse_order GET /back_office/warehouse/orders/:id(.:format) warehouse/orders#show +# PATCH /back_office/warehouse/orders/:id(.:format) warehouse/orders#update +# PUT /back_office/warehouse/orders/:id(.:format) warehouse/orders#update +# DELETE /back_office/warehouse/orders/:id(.:format) warehouse/orders#destroy +# map_fields_warehouse_batch GET /back_office/warehouse/batches/:id/map(.:format) warehouse/batches#map_fields +# set_mapping_warehouse_batch POST /back_office/warehouse/batches/:id/set_mapping(.:format) warehouse/batches#set_mapping +# process_confirm_warehouse_batch GET /back_office/warehouse/batches/:id/process(.:format) warehouse/batches#process_form +# process_warehouse_batch POST /back_office/warehouse/batches/:id/process(.:format) warehouse/batches#process_batch +# warehouse_batches GET /back_office/warehouse/batches(.:format) warehouse/batches#index +# POST /back_office/warehouse/batches(.:format) warehouse/batches#create +# new_warehouse_batch GET /back_office/warehouse/batches/new(.:format) warehouse/batches#new +# edit_warehouse_batch GET /back_office/warehouse/batches/:id/edit(.:format) warehouse/batches#edit +# warehouse_batch GET /back_office/warehouse/batches/:id(.:format) warehouse/batches#show +# PATCH /back_office/warehouse/batches/:id(.:format) warehouse/batches#update +# PUT /back_office/warehouse/batches/:id(.:format) warehouse/batches#update +# DELETE /back_office/warehouse/batches/:id(.:format) warehouse/batches#destroy +# warehouse_skus GET /back_office/warehouse/skus(.:format) warehouse/skus#index +# POST /back_office/warehouse/skus(.:format) warehouse/skus#create +# new_warehouse_sku GET /back_office/warehouse/skus/new(.:format) warehouse/skus#new +# edit_warehouse_sku GET /back_office/warehouse/skus/:id/edit(.:format) warehouse/skus#edit +# warehouse_sku GET /back_office/warehouse/skus/:id(.:format) warehouse/skus#show +# PATCH /back_office/warehouse/skus/:id(.:format) warehouse/skus#update +# PUT /back_office/warehouse/skus/:id(.:format) warehouse/skus#update +# DELETE /back_office/warehouse/skus/:id(.:format) warehouse/skus#destroy +# users GET /back_office/users(.:format) users#index +# POST /back_office/users(.:format) users#create +# new_user GET /back_office/users/new(.:format) users#new +# edit_user GET /back_office/users/:id/edit(.:format) users#edit +# user GET /back_office/users/:id(.:format) users#show +# PATCH /back_office/users/:id(.:format) users#update +# PUT /back_office/users/:id(.:format) users#update +# DELETE /back_office/users/:id(.:format) users#destroy +# set_as_home_return_address POST /back_office/return_addresses/:id/set_as_home(.:format) return_addresses#set_as_home +# return_addresses GET /back_office/return_addresses(.:format) return_addresses#index +# POST /back_office/return_addresses(.:format) return_addresses#create +# new_return_address GET /back_office/return_addresses/new(.:format) return_addresses#new +# edit_return_address GET /back_office/return_addresses/:id/edit(.:format) return_addresses#edit +# return_address GET /back_office/return_addresses/:id(.:format) return_addresses#show +# PATCH /back_office/return_addresses/:id(.:format) return_addresses#update +# PUT /back_office/return_addresses/:id(.:format) return_addresses#update +# DELETE /back_office/return_addresses/:id(.:format) return_addresses#destroy +# root GET /back_office(.:format) static_pages#index +# signout DELETE /back_office/signout(.:format) sessions#destroy +# login GET /back_office/login(.:format) static_pages#login +# slack_auth GET /auth/slack(.:format) sessions#new +# auth_slack_callback GET /auth/slack/callback(.:format) sessions#create +# public_root GET / public/static_pages#root +# public_login GET /login(.:format) public/static_pages#login +# send_email POST /login(.:format) public/sessions#send_email +# login_code GET /login/:token(.:format) public/sessions#login_code +# public_logout DELETE /logout(.:format) public/sessions#destroy +# my_mail GET /my/mail(.:format) public/mail#index +# revoke_confirm_public_api_key GET /my/api_keys/:id/revoke(.:format) public/api_keys#revoke_confirm +# revoke_public_api_key POST /my/api_keys/:id/revoke(.:format) public/api_keys#revoke +# public_api_keys GET /my/api_keys(.:format) public/api_keys#index +# POST /my/api_keys(.:format) public/api_keys#create +# new_public_api_key GET /my/api_keys/new(.:format) public/api_keys#new +# public_api_key GET /my/api_keys/:id(.:format) public/api_keys#show +# this_week_leaderboards GET /leaderboards/this_week(.:format) public/leaderboards#this_week +# this_month_leaderboards GET /leaderboards/this_month(.:format) public/leaderboards#this_month +# all_time_leaderboards GET /leaderboards/all_time(.:format) public/leaderboards#all_time +# public_mark_received_letter POST /letters/:id/mark_received(.:format) public_/letters#mark_received +# public_mark_mailed_letter POST /letters/:id/mark_mailed(.:format) public_/letters#mark_mailed +# GET /letters/:id(.:format) public_/letters#show +# show_lsv GET /lsv/:slug/:id(.:format) public/lsv#show +# public_letter GET /letters/:id(.:format) public/letters#show +# public_package GET /packages/:id(.:format) public/packages#show +# public_impersonate_form GET /impersonate(.:format) public/impersonations#new +# public_impersonate POST /impersonate(.:format) public/impersonations#create +# public_stop_impersonating GET /stop_impersonating(.:format) public/impersonations#stop_impersonating +# GET /:public_id(.:format) public/public_identifiable#show {:public_id=>/(pkg|ltr)![^\/]+/} +# cert_qz_tray GET /qz_tray/cert(.:format) qz_trays#cert +# settings_qz_tray GET /qz_tray/settings(.:format) qz_trays#settings +# sign_qz_tray POST /qz_tray/sign(.:format) qz_trays#sign +# test_print_qz_tray GET /qz_tray/test_print(.:format) qz_trays#test_print +# rails_health_check GET /up(.:format) rails/health#show +# usps_iv_mtr POST /webhooks/usps/iv_mtr(.:format) usps/iv_mtr/webhook#ingest +# public_v1_me GET /api/public/v1/me(.:format) public/api/v1/users#me {:format=>:json} +# public_v1_letters GET /api/public/v1/letters(.:format) public/api/v1/letters#index {:format=>:json} +# public_v1_letter GET /api/public/v1/letters/:id(.:format) public/api/v1/letters#show {:format=>:json} +# public_v1_packages GET /api/public/v1/packages(.:format) public/api/v1/packages#index {:format=>:json} +# public_v1_package GET /api/public/v1/packages/:id(.:format) public/api/v1/packages#show {:format=>:json} +# public_v1_mail_index GET /api/public/v1/mail(.:format) public/api/v1/mail#index {:format=>:json} +# public_v1_lsv_index GET /api/public/v1/lsv(.:format) public/api/v1/lsv#index {:format=>:json} +# public_v1_lsv GET /api/public/v1/lsv/:slug/:id(.:format) public/api/v1/lsv#show {:format=>:json} +# api_v1_me GET /api/public/v1/me(.:format) api/public/api/v1/users#me {:format=>:json} +# new_api_v1_user GET /api/v1/user/new(.:format) api/v1/users#new {:format=>:json} +# edit_api_v1_user GET /api/v1/user/edit(.:format) api/v1/users#edit {:format=>:json} +# api_v1_user GET /api/v1/user(.:format) api/v1/users#show {:format=>:json} +# PATCH /api/v1/user(.:format) api/v1/users#update {:format=>:json} +# PUT /api/v1/user(.:format) api/v1/users#update {:format=>:json} +# DELETE /api/v1/user(.:format) api/v1/users#destroy {:format=>:json} +# POST /api/v1/user(.:format) api/v1/users#create {:format=>:json} +# mark_printed_api_v1_letter POST /api/v1/letters/:id/mark_printed(.:format) api/v1/letters#mark_printed {:format=>:json} +# api_v1_letters GET /api/v1/letters(.:format) api/v1/letters#index {:format=>:json} +# POST /api/v1/letters(.:format) api/v1/letters#create {:format=>:json} +# new_api_v1_letter GET /api/v1/letters/new(.:format) api/v1/letters#new {:format=>:json} +# edit_api_v1_letter GET /api/v1/letters/:id/edit(.:format) api/v1/letters#edit {:format=>:json} +# api_v1_letter GET /api/v1/letters/:id(.:format) api/v1/letters#show {:format=>:json} +# PATCH /api/v1/letters/:id(.:format) api/v1/letters#update {:format=>:json} +# PUT /api/v1/letters/:id(.:format) api/v1/letters#update {:format=>:json} +# DELETE /api/v1/letters/:id(.:format) api/v1/letters#destroy {:format=>:json} +# create_instant_letter_api_v1_letter_queues POST /api/v1/letter_queues/instant/:id(.:format) api/v1/letter_queues#create_instant_letter {:format=>:json} +# show_queued_api_v1_letter_queues GET /api/v1/letter_queues/instant/:id/queued(.:format) api/v1/letter_queues#queued {:format=>:json} +# POST /api/v1/letter_queues/:id(.:format) api/v1/letter_queues#create_letter {:format=>:json} +# api_v1_letter_queues GET /api/v1/letter_queues(.:format) api/v1/letter_queues#index {:format=>:json} +# POST /api/v1/letter_queues(.:format) api/v1/letter_queues#create {:format=>:json} +# api_v1_letter_queue GET /api/v1/letter_queues/:id(.:format) api/v1/letter_queues#show {:format=>:json} +# PATCH /api/v1/letter_queues/:id(.:format) api/v1/letter_queues#update {:format=>:json} +# PUT /api/v1/letter_queues/:id(.:format) api/v1/letter_queues#update {:format=>:json} +# DELETE /api/v1/letter_queues/:id(.:format) api/v1/letter_queues#destroy {:format=>:json} +# cert_api_v1_qz_tray GET /api/v1/qz_tray/cert(.:format) api/v1/qz_trays#cert {:format=>:json} +# sign_api_v1_qz_tray POST /api/v1/qz_tray/sign(.:format) api/v1/qz_trays#sign {:format=>:json} +# api_v1_tags GET /api/v1/tags(.:format) api/v1/tags#index {:format=>:json} +# api_v1_tag GET /api/v1/tags/:id(.:format) api/v1/tags#show {:format=>:json} +# letter_opener_web /letter_opener LetterOpenerWeb::Engine +# turbo_recede_historical_location GET /recede_historical_location(.:format) turbo/native/navigation#recede +# turbo_resume_historical_location GET /resume_historical_location(.:format) turbo/native/navigation#resume +# turbo_refresh_historical_location GET /refresh_historical_location(.:format) turbo/native/navigation#refresh +# rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create +# rails_relay_inbound_emails POST /rails/action_mailbox/relay/inbound_emails(.:format) action_mailbox/ingresses/relay/inbound_emails#create +# rails_sendgrid_inbound_emails POST /rails/action_mailbox/sendgrid/inbound_emails(.:format) action_mailbox/ingresses/sendgrid/inbound_emails#create +# rails_mandrill_inbound_health_check GET /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#health_check +# rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create +# rails_mailgun_inbound_emails POST /rails/action_mailbox/mailgun/inbound_emails/mime(.:format) action_mailbox/ingresses/mailgun/inbound_emails#create +# rails_conductor_inbound_emails GET /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#index +# POST /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#create +# new_rails_conductor_inbound_email GET /rails/conductor/action_mailbox/inbound_emails/new(.:format) rails/conductor/action_mailbox/inbound_emails#new +# rails_conductor_inbound_email GET /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#show +# new_rails_conductor_inbound_email_source GET /rails/conductor/action_mailbox/inbound_emails/sources/new(.:format) rails/conductor/action_mailbox/inbound_emails/sources#new +# rails_conductor_inbound_email_sources POST /rails/conductor/action_mailbox/inbound_emails/sources(.:format) rails/conductor/action_mailbox/inbound_emails/sources#create +# rails_conductor_inbound_email_reroute POST /rails/conductor/action_mailbox/:inbound_email_id/reroute(.:format) rails/conductor/action_mailbox/reroutes#create +# rails_conductor_inbound_email_incinerate POST /rails/conductor/action_mailbox/:inbound_email_id/incinerate(.:format) rails/conductor/action_mailbox/incinerates#create +# rails_service_blob GET /rails/active_storage/blobs/redirect/:signed_id/*filename(.:format) active_storage/blobs/redirect#show +# rails_service_blob_proxy GET /rails/active_storage/blobs/proxy/:signed_id/*filename(.:format) active_storage/blobs/proxy#show +# GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs/redirect#show +# rails_blob_representation GET /rails/active_storage/representations/redirect/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/redirect#show +# rails_blob_representation_proxy GET /rails/active_storage/representations/proxy/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/proxy#show +# GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/redirect#show +# rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show +# update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update +# rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create +# +# Routes for GoodJob::Engine: +# root GET / good_job/jobs#redirect_to_index +# mass_update_jobs GET /jobs/mass_update(.:format) redirect(301, path: jobs) +# PUT /jobs/mass_update(.:format) good_job/jobs#mass_update +# discard_job PUT /jobs/:id/discard(.:format) good_job/jobs#discard +# force_discard_job PUT /jobs/:id/force_discard(.:format) good_job/jobs#force_discard +# reschedule_job PUT /jobs/:id/reschedule(.:format) good_job/jobs#reschedule +# retry_job PUT /jobs/:id/retry(.:format) good_job/jobs#retry +# jobs GET /jobs(.:format) good_job/jobs#index +# job GET /jobs/:id(.:format) good_job/jobs#show +# DELETE /jobs/:id(.:format) good_job/jobs#destroy +# metrics_primary_nav GET /jobs/metrics/primary_nav(.:format) good_job/metrics#primary_nav +# metrics_job_status GET /jobs/metrics/job_status(.:format) good_job/metrics#job_status +# retry_batch PUT /batches/:id/retry(.:format) good_job/batches#retry +# batches GET /batches(.:format) good_job/batches#index +# batch GET /batches/:id(.:format) good_job/batches#show +# enqueue_cron_entry POST /cron_entries/:cron_key/enqueue(.:format) good_job/cron_entries#enqueue +# enable_cron_entry PUT /cron_entries/:cron_key/enable(.:format) good_job/cron_entries#enable +# disable_cron_entry PUT /cron_entries/:cron_key/disable(.:format) good_job/cron_entries#disable +# cron_entries GET /cron_entries(.:format) good_job/cron_entries#index +# cron_entry GET /cron_entries/:cron_key(.:format) good_job/cron_entries#show +# processes GET /processes(.:format) good_job/processes#index +# performance_index GET /performance(.:format) good_job/performance#index +# performance GET /performance/:id(.:format) good_job/performance#show +# pauses POST /pauses(.:format) good_job/pauses#create +# DELETE /pauses(.:format) good_job/pauses#destroy +# GET /pauses(.:format) good_job/pauses#index +# cleaner_index GET /cleaner(.:format) good_job/cleaner#index +# frontend_module GET /frontend/modules/:version/:id(.:format) good_job/frontends#module {:version=>"4-9-3", :format=>"js"} +# frontend_static GET /frontend/static/:version/:id(.:format) good_job/frontends#static {:version=>"4-9-3"} +# +# Routes for Blazer::Engine: +# run_queries POST /queries/run(.:format) blazer/queries#run +# cancel_queries POST /queries/cancel(.:format) blazer/queries#cancel +# refresh_query POST /queries/:id/refresh(.:format) blazer/queries#refresh +# tables_queries GET /queries/tables(.:format) blazer/queries#tables +# schema_queries GET /queries/schema(.:format) blazer/queries#schema +# docs_queries GET /queries/docs(.:format) blazer/queries#docs +# queries GET /queries(.:format) blazer/queries#index +# POST /queries(.:format) blazer/queries#create +# new_query GET /queries/new(.:format) blazer/queries#new +# edit_query GET /queries/:id/edit(.:format) blazer/queries#edit +# query GET /queries/:id(.:format) blazer/queries#show +# PATCH /queries/:id(.:format) blazer/queries#update +# PUT /queries/:id(.:format) blazer/queries#update +# DELETE /queries/:id(.:format) blazer/queries#destroy +# run_check GET /checks/:id/run(.:format) blazer/checks#run +# checks GET /checks(.:format) blazer/checks#index +# POST /checks(.:format) blazer/checks#create +# new_check GET /checks/new(.:format) blazer/checks#new +# edit_check GET /checks/:id/edit(.:format) blazer/checks#edit +# check PATCH /checks/:id(.:format) blazer/checks#update +# PUT /checks/:id(.:format) blazer/checks#update +# DELETE /checks/:id(.:format) blazer/checks#destroy +# refresh_dashboard POST /dashboards/:id/refresh(.:format) blazer/dashboards#refresh +# dashboards POST /dashboards(.:format) blazer/dashboards#create +# new_dashboard GET /dashboards/new(.:format) blazer/dashboards#new +# edit_dashboard GET /dashboards/:id/edit(.:format) blazer/dashboards#edit +# dashboard GET /dashboards/:id(.:format) blazer/dashboards#show +# PATCH /dashboards/:id(.:format) blazer/dashboards#update +# PUT /dashboards/:id(.:format) blazer/dashboards#update +# DELETE /dashboards/:id(.:format) blazer/dashboards#destroy +# root GET / blazer/queries#home +# +# Routes for LetterOpenerWeb::Engine: +# letters GET / letter_opener_web/letters#index +# clear_letters POST /clear(.:format) letter_opener_web/letters#clear +# letter GET /:id(/:style)(.:format) letter_opener_web/letters#show +# delete_letter POST /:id/delete(.:format) letter_opener_web/letters#destroy +# GET /:id/attachments/:file(.:format) letter_opener_web/letters#attachment {:file=>/[^\/]+/} + +class AdminConstraint + def self.matches?(request) + return false unless request.session[:user_id] + + user = User.find_by(id: request.session[:user_id]) + user&.admin? + end +end + +Rails.application.routes.draw do + get "customs_receipts/index" + get "customs_receipts/show" + scope path: "back_office" do + resources :public_ids, only: [:index] do + collection do + post :lookup + end + end + + namespace :inspect do + resources :iv_mtr_events, only: [:show] + resources :indicia, only: [:show] + end + scope :my do + resource :tasks, only: %i(show) do + get :badge + post :refresh + end + end + get "/tags", to: "tags#index" + get "/tags/:id", to: "tags#show", as: :tag_stats + post "/tags/refresh", to: "tags#refresh", as: :refresh_tags + resources :customs_receipts, only: [:index] do + collection do + get :generate + end + end + resources :letters do + member do + post :generate_label + post :buy_indicia + post :mark_printed + post :mark_mailed + post :mark_received + post :clear_label + get :preview_template if Rails.env.development? + end + end + namespace :letter do + resources :batches do + member do + get "/map", to: "batches#map_fields", as: :map_fields + post :set_mapping + get "/process", to: "batches#process_form", as: :process_confirm + post "/process", to: "batches#process_batch", as: :process + post :mark_printed + post :mark_mailed + post :update_costs + get :regen, to: "batches#regenerate_form", as: :regenerate_form + post :regen, to: "batches#regenerate_labels", as: :regenerate_labels + end + end + resources :queues do + collection do + post :mark_printed_instants_mailed + end + member do + post :batch, as: :make_batch_from + end + end + resources :instant_queues, controller: "instant_queues" + end + resources :api_keys do + member do + get "/revoke", to: "api_keys#revoke_confirm", as: :revoke_confirm + post :revoke + end + end + + namespace :admin do + resources :addresses + resources :return_addresses + resources :source_tags + resources :users + + namespace :warehouse do + resources :templates + resources :orders + resources :skus + end + + namespace :usps do + resources :mailer_ids + resources :payment_accounts + end + + resources :common_tags + + root to: "users#index" + end + + constraints AdminConstraint do + mount GoodJob::Engine => "good_job" + mount Blazer::Engine, at: "blazer" + get "/impersonate/:id", to: "sessions#impersonate", as: :impersonate_user + end + get "/stop_impersonating", to: "sessions#stop_impersonating", as: :stop_impersonating + + namespace :usps do + resources :indicia + resources :payment_accounts + resources :mailer_ids + end + resources :source_tags + namespace :warehouse do + resources :templates + resources :orders do + member do + get :cancel + post :cancel, to: "orders#confirm_cancel" + post "send_to_warehouse" + end + end + resources :batches do + member do + get "/map", to: "batches#map_fields", as: :map_fields + post :set_mapping + get "/process", to: "batches#process_form", as: :process_confirm + post "/process", to: "batches#process_batch", as: :process + end + end + resources :skus + end + resources :users + resources :return_addresses do + member do + post :set_as_home + end + end + root "static_pages#index" + + delete "signout", to: "sessions#destroy", as: :signout + get "/login" => "static_pages#login" + end + + get "/auth/slack", to: "sessions#new", as: :slack_auth + get "/auth/slack/callback", to: "sessions#create" + + root "public/static_pages#root", as: :public_root + + get "/login" => "public/static_pages#login", as: :public_login + post "/login" => "public/sessions#send_email", as: :send_email + get "/login/:token", to: "public/sessions#login_code", as: :login_code + delete "logout", to: "public/sessions#destroy", as: :public_logout + + scope :my do + get "/mail", to: "public/mail#index", as: :my_mail + resources :api_keys, module: :public, only: [:index, :new, :create, :show], as: :public_api_keys do + member do + get "/revoke", to: "api_keys#revoke_confirm", as: :revoke_confirm + post :revoke + end + end + end + + resources :leaderboards, module: :public, only: [] do + collection do + get "this_week" + get "this_month" + get "all_time" + end + end + + resources "letters", module: :public_, only: [:show] do + member do + post :mark_received, as: :public_mark_received + post :mark_mailed, as: :public_mark_mailed + end + end + + resource :map, only: [:show], module: :public + + get "/lsv/:slug/:id", to: "public/lsv#show", as: :show_lsv + get "/lsv/msr/:id/customs_receipt", to: "public/lsv#customs_receipt", as: :msr_customs_receipt + post "/lsv/msr/:id/customs_receipt", to: "public/lsv#generate_customs_receipt", as: :msr_generate_customs_receipt + + get "/packages/:id/customs_receipt", to: "public/packages#customs_receipt", as: :package_customs_receipt + post "/packages/:id/customs_receipt", to: "public/packages#generate_customs_receipt", as: :package_generate_customs_receipt + + get "/letters/:id", to: "public/letters#show", as: :public_letter + get "/packages/:id", to: "public/packages#show", as: :public_package + + get "/impersonate", to: "public/impersonations#new", as: :public_impersonate_form + post "/impersonate", to: "public/impersonations#create", as: :public_impersonate + get "/stop_impersonating", to: "public/impersonations#stop_impersonating", as: :public_stop_impersonating + + get "/:public_id", to: "public/public_identifiable#show", constraints: { public_id: /(pkg|ltr)![^\/]+/ } + + resource :qz_tray, only: [] do + get :cert + get :settings + post :sign + get :test_print + end + # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. + # Can be used by load balancers and uptime monitors to verify that the app is live. + get "up" => "rails/health#show", as: :rails_health_check + + scope :webhooks do + namespace :usps do + namespace :iv_mtr do + post "", to: "webhook#ingest" + end + end + end + + scope :api do + defaults format: :json do + namespace :public do + scope "", module: :api do + namespace :v1 do + get :me, to: "users#me" + resources :letters, only: [:index, :show] + resources :packages, only: [:index, :show] + resources :mail, only: [:index] + resources :lsv, only: [:index] + get "/lsv/:slug/:id", to: "lsv#show", as: :lsv + end + end + end + end + end + namespace :api do + defaults format: :json do + scope :public, module: :public do + scope "", module: :api do + namespace :v1 do + get :me, to: "users#me" + end + end + end + namespace :v1 do + resource :user + resources :letters do + member do + post :mark_printed + end + end + resources :letter_queues, only: [:index, :show, :create, :update, :destroy] do + collection do + post "instant/:id", to: "letter_queues#create_instant_letter", as: :create_instant_letter + get "instant/:id/queued", to: "letter_queues#queued", as: :show_queued + end + member do + post "", to: "letter_queues#create_letter" + end + end + resource :qz_tray, only: [] do + get :cert + post :sign + end + resources :tags, only: [:index, :show] do + member do + get :letters + end + end + end + end + end + + # Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb) + # get "manifest" => "rails/pwa#manifest", as: :pwa_manifest + # get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker + + # Defines the root path route ("/") + # root "posts#index" + if Rails.env.development? + mount LetterOpenerWeb::Engine, at: "/letter_opener" + end +end diff --git a/config/storage.yml b/config/storage.yml new file mode 100644 index 0000000..e771e16 --- /dev/null +++ b/config/storage.yml @@ -0,0 +1,42 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +hetzner: + service: S3 + endpoint: "https://hel1.your-objectstorage.com" + access_key_id: <%= Rails.application.credentials.dig(:hetzner, :access_key_id) %> + secret_access_key: <%= Rails.application.credentials.dig(:hetzner, :secret_access_key) %> + region: hel1 + bucket: hackclub-nora-theseus + +# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket-<%= Rails.env %> + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket-<%= Rails.env %> + +# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# microsoft: +# service: AzureStorage +# storage_account_name: your_account_name +# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> +# container: your_container_name-<%= Rails.env %> + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/config/vite.json b/config/vite.json new file mode 100644 index 0000000..082213a --- /dev/null +++ b/config/vite.json @@ -0,0 +1,21 @@ +{ + "all": { + "sourceCodeDir": "app/frontend", + "watchAdditionalPaths": [], + "packageManager": "yarn" + }, + "development": { + "autoBuild": true, + "publicOutputDir": "vite-dev", + "port": 3036 + }, + "test": { + "autoBuild": true, + "publicOutputDir": "vite-test", + "port": 3037 + }, + "production": { + "publicOutputDir": "", + "port": 3038 + } +} diff --git a/credentials.yml.enc b/credentials.yml.enc new file mode 100644 index 0000000..ae12695 --- /dev/null +++ b/credentials.yml.enc @@ -0,0 +1 @@ +SAS06A+S9fb5YYLGFFpuKGU3d3rROBI87ge+7E9WjjCY5nceObY+cOgtsvfZA/fpZttai40ykMZsFJLXX8J1O/5Lq13Odpf0E65jWqmK7nG1QnfiUf/ZkfO4Fqj3cFcSOQBB/qFLzxVvcI5Dd9155SSPIr3nnoHMxch6bSsk2HDfB+qxaVFOnrnIICeFvXn4UUXpFhOoO8FR4UM7FDYoXzY0JYka7BLN0ypyvCHQYxu9Mj4QBVVqxrNkszEQlCxVaiTUy1G3SNonSnCqyJaAQxSQ90g/JjsA9KbKsjzRqTtw9IhiyiO1ZDHfIi6fchVx4bnNRoOUff4A6bjPeWUiQzvkr0Sywu0vMDRcqn+ug3m5hK5kVhQ1KLMKCNX0uKyEzDOMD4tTOslGzVcgVthpqxSoRkiQNOhq0/NNdx1E05Wx24SXdTP2q50DZOQwlK00P+0FiTTJKY5bac/6cpGP33f32UTE3PrklowiEKB2l/KrevheJ2cuY718PGzc0G4qYcNCPbY2Ki6dseLSm2fUU/4AEKL1WdnQfqQcTwZQ41+lkocgCHzdhMvs/WLb2OUEvbkrK6sR/IVu/5ZIZ4MvVW6wSmgjywFNLSbJuG+eMGqD+1LPRK2awAA5N1SnjLFjkO72aYIDWBwC5MLpYUxp80SnB2gB66e+7qjVpg5JDzX26lmJYsJEcrzwLnW0C1qmbEgKYxe74K9++ZDlv3iKgJSQHXaz5bLAYXy1p4SYFvXqSbUFKSqewI18rVQyiDeTObEkovclGbW3Vmoatl4U/gQ8LaTRHlHwW0lLLbC2cSol6w3cGu4EkMTEQt/cObBZf8oSKxzgC3eNF1jSPLCnI5lBePXV29xhZzc8MI6X+NjQ6TQrNmy8GUKhkydebLyMYxrZB7jXT+PVDm4kgNhLTQvKkuqlsiH8gAr8BUh0YoowZ5MTunNuvW+nf4eRIzig7fjxgXRbS4clVOgdZWPELNhVaIKkLzmx/L6mfUoTH5204c87jTmxg7d+KYXOKTW+DSmpILefGPCDW1iL9LW9dCv0RFlVrV4+bU48wiQX+NDzBOsT7u1OASxlPf4W87c1hmZwHsrWA/2656851QJGWT+ZKsSgNZM3SZZHqOgVOmzyKBrW1vToSk3H+3N8t/CRbOfxRHjM0VGxSsvePvEWXJVEUvuQ7QPYJ5mzaljT5vhCEn+Zl4F8licIC7zzpgFgYu2aesDdz011PJrVn1Vm13Njffk54SlrFMbcWRr4iwpF71UMOLtDg70WEoAiQxKSVQG6r/9R3w3rKTyH9dSW0Ir54GIH6xh6tjFHo3gvXG1xsgx7daLZ05aPZv25DIgPkMGo3DP8oohJbnb5qJVpKtwL2Bvye4nEZj/NcVZAyrJJGy4tMui8PuKOHH7cQ8vApBEl8HQZ17JYAwBY/yfPMFYpSkxiyC3s3tPDDg4ur9oJatfdEBKANMKz5FUs9/WHzWEGqPaEaVuuvc7CfbNEydlzLesLt5bLYOJOY1tLD1lqMJsGtalf/cBTpyYatH668l8xrUWwhB0J6b75x5pTVZzc2FPVDr2cwNrn9aL2/gScKLrI3hTB2KoyOVxLQdUGr9BI3aqw1pq0+CU2m5M6qtu7PiqViqaDdcYTNJOvuwjeZV/wUTg8Izt/7UxywT/nrWBW8nMqGYFG7xFrmXJRn89jSxfjzBRfQi3VA+lwfaSubZ+YLITpgsmVwENsqY3AUMi59BxGQui1J4bft52HNUIWsSkUJSn+aV6hoFPYQfqAs4mpmb3ah+RDAEB/BSWo61fY/SHddrZW6nZRcPniveVRs63yVRuu/ZQhFUcsujrjm+WZbZanE2hmGeEP26E4I+OzY9m8EaPisZ+tVp7VfjxxcvCoaCM3v12s20dG8KspZaR+ODVfUWKnmGz9imzsNOPmCw==--qNCiPmhSpvlsXRDx--Q6ZPwKDDb2cubp5wMOqzow== \ No newline at end of file diff --git a/db/cable_schema.rb b/db/cable_schema.rb new file mode 100644 index 0000000..2366660 --- /dev/null +++ b/db/cable_schema.rb @@ -0,0 +1,11 @@ +ActiveRecord::Schema[7.1].define(version: 1) do + create_table "solid_cable_messages", force: :cascade do |t| + t.binary "channel", limit: 1024, null: false + t.binary "payload", limit: 536870912, null: false + t.datetime "created_at", null: false + t.integer "channel_hash", limit: 8, null: false + t.index ["channel"], name: "index_solid_cable_messages_on_channel" + t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash" + t.index ["created_at"], name: "index_solid_cable_messages_on_created_at" + end +end diff --git a/db/cache_schema.rb b/db/cache_schema.rb new file mode 100644 index 0000000..6005a29 --- /dev/null +++ b/db/cache_schema.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +ActiveRecord::Schema[7.2].define(version: 1) do + create_table "solid_cache_entries", force: :cascade do |t| + t.binary "key", limit: 1024, null: false + t.binary "value", limit: 536870912, null: false + t.datetime "created_at", null: false + t.integer "key_hash", limit: 8, null: false + t.integer "byte_size", limit: 4, null: false + t.index ["byte_size"], name: "index_solid_cache_entries_on_byte_size" + t.index ["key_hash", "byte_size"], name: "index_solid_cache_entries_on_key_hash_and_byte_size" + t.index ["key_hash"], name: "index_solid_cache_entries_on_key_hash", unique: true + end +end diff --git a/db/migrate.zip b/db/migrate.zip new file mode 100644 index 0000000..4f2ef1c Binary files /dev/null and b/db/migrate.zip differ diff --git a/db/migrate/20250601023121_init_schema.rb b/db/migrate/20250601023121_init_schema.rb new file mode 100644 index 0000000..49c1598 --- /dev/null +++ b/db/migrate/20250601023121_init_schema.rb @@ -0,0 +1,555 @@ +class InitSchema < ActiveRecord::Migration[8.0] + def up + # These are extensions that must be enabled in order to support this database + enable_extension "citext" + enable_extension "pg_catalog.plpgsql" + enable_extension "pgcrypto" + create_table "active_storage_attachments" do |t| + t.string "name", null: false + t.string "record_type", null: false + t.bigint "record_id", null: false + t.bigint "blob_id", null: false + t.datetime "created_at", null: false + t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" + t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true + end + create_table "active_storage_blobs" do |t| + t.string "key", null: false + t.string "filename", null: false + t.string "content_type" + t.text "metadata" + t.string "service_name", null: false + t.bigint "byte_size", null: false + t.string "checksum" + t.datetime "created_at", null: false + t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true + end + create_table "active_storage_variant_records" do |t| + t.bigint "blob_id", null: false + t.string "variation_digest", null: false + t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true + end + create_table "addresses" do |t| + t.string "first_name" + t.string "last_name" + t.string "line_1" + t.string "line_2" + t.string "city" + t.string "state" + t.string "postal_code" + t.integer "country" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "phone_number" + t.bigint "batch_id" + t.string "email" + t.index ["batch_id"], name: "index_addresses_on_batch_id" + end + create_table "api_keys" do |t| + t.bigint "user_id", null: false + t.datetime "revoked_at" + t.boolean "pii" + t.text "token_ciphertext" + t.string "token_bidx" + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "may_impersonate" + t.index ["token_bidx"], name: "index_api_keys_on_token_bidx", unique: true + t.index ["user_id"], name: "index_api_keys_on_user_id" + end + create_table "batches" do |t| + t.bigint "user_id", null: false + t.jsonb "field_mapping" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "type", null: false + t.bigint "warehouse_template_id" + t.integer "address_count" + t.string "warehouse_user_facing_title" + t.string "aasm_state" + t.decimal "letter_height" + t.decimal "letter_width" + t.decimal "letter_weight" + t.integer "letter_processing_category" + t.bigint "letter_mailer_id_id" + t.bigint "letter_return_address_id" + t.citext "tags", default: [], array: true + t.date "letter_mailing_date" + t.string "letter_return_address_name" + t.bigint "letter_queue_id" + t.index ["letter_mailer_id_id"], name: "index_batches_on_letter_mailer_id_id" + t.index ["letter_queue_id"], name: "index_batches_on_letter_queue_id" + t.index ["letter_return_address_id"], name: "index_batches_on_letter_return_address_id" + t.index ["tags"], name: "index_batches_on_tags", using: :gin + t.index ["type"], name: "index_batches_on_type" + t.index ["user_id"], name: "index_batches_on_user_id" + t.index ["warehouse_template_id"], name: "index_batches_on_warehouse_template_id" + end + create_table "blazer_audits" do |t| + t.bigint "user_id" + t.bigint "query_id" + t.text "statement" + t.string "data_source" + t.datetime "created_at" + t.index ["query_id"], name: "index_blazer_audits_on_query_id" + t.index ["user_id"], name: "index_blazer_audits_on_user_id" + end + create_table "blazer_checks" do |t| + t.bigint "creator_id" + t.bigint "query_id" + t.string "state" + t.string "schedule" + t.text "emails" + t.text "slack_channels" + t.string "check_type" + t.text "message" + t.datetime "last_run_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["creator_id"], name: "index_blazer_checks_on_creator_id" + t.index ["query_id"], name: "index_blazer_checks_on_query_id" + end + create_table "blazer_dashboard_queries" do |t| + t.bigint "dashboard_id" + t.bigint "query_id" + t.integer "position" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["dashboard_id"], name: "index_blazer_dashboard_queries_on_dashboard_id" + t.index ["query_id"], name: "index_blazer_dashboard_queries_on_query_id" + end + create_table "blazer_dashboards" do |t| + t.bigint "creator_id" + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["creator_id"], name: "index_blazer_dashboards_on_creator_id" + end + create_table "blazer_queries" do |t| + t.bigint "creator_id" + t.string "name" + t.text "description" + t.text "statement" + t.string "data_source" + t.string "status" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["creator_id"], name: "index_blazer_queries_on_creator_id" + end + create_table "common_tags" do |t| + t.string "tag" + t.boolean "implies_ysws" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "good_job_batches", id: :uuid, default: -> { "gen_random_uuid()" } do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.text "description" + t.jsonb "serialized_properties" + t.text "on_finish" + t.text "on_success" + t.text "on_discard" + t.text "callback_queue_name" + t.integer "callback_priority" + t.datetime "enqueued_at" + t.datetime "discarded_at" + t.datetime "finished_at" + t.datetime "jobs_finished_at" + end + create_table "good_job_executions", id: :uuid, default: -> { "gen_random_uuid()" } do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.uuid "active_job_id", null: false + t.text "job_class" + t.text "queue_name" + t.jsonb "serialized_params" + t.datetime "scheduled_at" + t.datetime "finished_at" + t.text "error" + t.integer "error_event", limit: 2 + t.text "error_backtrace", array: true + t.uuid "process_id" + t.interval "duration" + t.index ["active_job_id", "created_at"], name: "index_good_job_executions_on_active_job_id_and_created_at" + t.index ["process_id", "created_at"], name: "index_good_job_executions_on_process_id_and_created_at" + end + create_table "good_job_processes", id: :uuid, default: -> { "gen_random_uuid()" } do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.jsonb "state" + t.integer "lock_type", limit: 2 + end + create_table "good_job_settings", id: :uuid, default: -> { "gen_random_uuid()" } do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.text "key" + t.jsonb "value" + t.index ["key"], name: "index_good_job_settings_on_key", unique: true + end + create_table "good_jobs", id: :uuid, default: -> { "gen_random_uuid()" } do |t| + t.text "queue_name" + t.integer "priority" + t.jsonb "serialized_params" + t.datetime "scheduled_at" + t.datetime "performed_at" + t.datetime "finished_at" + t.text "error" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.uuid "active_job_id" + t.text "concurrency_key" + t.text "cron_key" + t.uuid "retried_good_job_id" + t.datetime "cron_at" + t.uuid "batch_id" + t.uuid "batch_callback_id" + t.boolean "is_discrete" + t.integer "executions_count" + t.text "job_class" + t.integer "error_event", limit: 2 + t.text "labels", array: true + t.uuid "locked_by_id" + t.datetime "locked_at" + t.index ["active_job_id", "created_at"], name: "index_good_jobs_on_active_job_id_and_created_at" + t.index ["batch_callback_id"], name: "index_good_jobs_on_batch_callback_id", where: "(batch_callback_id IS NOT NULL)" + t.index ["batch_id"], name: "index_good_jobs_on_batch_id", where: "(batch_id IS NOT NULL)" + t.index ["concurrency_key", "created_at"], name: "index_good_jobs_on_concurrency_key_and_created_at" + t.index ["concurrency_key"], name: "index_good_jobs_on_concurrency_key_when_unfinished", where: "(finished_at IS NULL)" + t.index ["cron_key", "created_at"], name: "index_good_jobs_on_cron_key_and_created_at_cond", where: "(cron_key IS NOT NULL)" + t.index ["cron_key", "cron_at"], name: "index_good_jobs_on_cron_key_and_cron_at_cond", unique: true, where: "(cron_key IS NOT NULL)" + t.index ["finished_at"], name: "index_good_jobs_jobs_on_finished_at", where: "((retried_good_job_id IS NULL) AND (finished_at IS NOT NULL))" + t.index ["labels"], name: "index_good_jobs_on_labels", where: "(labels IS NOT NULL)", using: :gin + t.index ["locked_by_id"], name: "index_good_jobs_on_locked_by_id", where: "(locked_by_id IS NOT NULL)" + t.index ["priority", "created_at"], name: "index_good_job_jobs_for_candidate_lookup", where: "(finished_at IS NULL)" + t.index ["priority", "created_at"], name: "index_good_jobs_jobs_on_priority_created_at_when_unfinished", order: { priority: "DESC NULLS LAST" }, where: "(finished_at IS NULL)" + t.index ["priority", "scheduled_at"], name: "index_good_jobs_on_priority_scheduled_at_unfinished_unlocked", where: "((finished_at IS NULL) AND (locked_by_id IS NULL))" + t.index ["queue_name", "scheduled_at"], name: "index_good_jobs_on_queue_name_and_scheduled_at", where: "(finished_at IS NULL)" + t.index ["scheduled_at"], name: "index_good_jobs_on_scheduled_at", where: "(finished_at IS NULL)" + end + create_table "letter_queues" do |t| + t.string "name" + t.string "slug" + t.bigint "user_id", null: false + t.decimal "letter_height" + t.decimal "letter_width" + t.decimal "letter_weight" + t.integer "letter_processing_category" + t.date "letter_mailing_date" + t.bigint "letter_mailer_id_id" + t.bigint "letter_return_address_id" + t.string "letter_return_address_name" + t.string "user_facing_title" + t.citext "tags", default: [], array: true + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "type" + t.string "template" + t.string "postage_type" + t.bigint "usps_payment_account_id" + t.boolean "include_qr_code", default: true + t.index ["letter_mailer_id_id"], name: "index_letter_queues_on_letter_mailer_id_id" + t.index ["letter_return_address_id"], name: "index_letter_queues_on_letter_return_address_id" + t.index ["type"], name: "index_letter_queues_on_type" + t.index ["user_id"], name: "index_letter_queues_on_user_id" + end + create_table "letters" do |t| + t.integer "processing_category" + t.text "body" + t.string "aasm_state" + t.bigint "usps_mailer_id_id", null: false + t.decimal "postage" + t.integer "imb_serial_number" + t.bigint "address_id", null: false + t.integer "imb_rollover_count" + t.string "recipient_email" + t.decimal "weight" + t.decimal "width" + t.decimal "height" + t.boolean "non_machinable" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.text "rubber_stamps" + t.bigint "batch_id" + t.bigint "return_address_id", null: false + t.jsonb "metadata" + t.citext "tags", default: [], array: true + t.integer "postage_type" + t.date "mailing_date" + t.string "user_facing_title" + t.datetime "printed_at" + t.datetime "mailed_at" + t.datetime "received_at" + t.bigint "user_id", null: false + t.string "return_address_name" + t.bigint "letter_queue_id" + t.string "idempotency_key" + t.index ["address_id"], name: "index_letters_on_address_id" + t.index ["batch_id"], name: "index_letters_on_batch_id" + t.index ["idempotency_key"], name: "index_letters_on_idempotency_key", unique: true + t.index ["imb_serial_number"], name: "index_letters_on_imb_serial_number" + t.index ["letter_queue_id"], name: "index_letters_on_letter_queue_id" + t.index ["return_address_id"], name: "index_letters_on_return_address_id" + t.index ["tags"], name: "index_letters_on_tags", using: :gin + t.index ["user_id"], name: "index_letters_on_user_id" + t.index ["usps_mailer_id_id"], name: "index_letters_on_usps_mailer_id_id" + end + create_table "public_api_keys" do |t| + t.bigint "public_user_id", null: false + t.string "token_ciphertext" + t.datetime "revoked_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "name" + t.string "token_bidx" + t.index ["public_user_id"], name: "index_public_api_keys_on_public_user_id" + t.index ["token_bidx"], name: "index_public_api_keys_on_token_bidx", unique: true + end + create_table "public_impersonations" do |t| + t.bigint "user_id", null: false + t.string "justification" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "target_email" + t.index ["user_id"], name: "index_public_impersonations_on_user_id" + end + create_table "public_login_codes" do |t| + t.string "token" + t.datetime "expires_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.bigint "user_id", null: false + t.datetime "used_at" + t.index ["user_id"], name: "index_public_login_codes_on_user_id" + end + create_table "public_users" do |t| + t.string "email" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "return_addresses" do |t| + t.string "name" + t.string "line_1" + t.string "line_2" + t.string "city" + t.string "state" + t.string "postal_code" + t.integer "country" + t.boolean "shared" + t.bigint "user_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_return_addresses_on_user_id" + end + create_table "source_tags" do |t| + t.string "slug" + t.string "name" + t.string "owner" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "users" do |t| + t.string "slack_id" + t.string "email" + t.boolean "is_admin" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "icon_url" + t.string "username" + t.boolean "can_warehouse" + t.boolean "can_impersonate_public" + t.bigint "home_mid_id", default: 1, null: false + t.bigint "home_return_address_id", default: 1, null: false + t.index ["home_mid_id"], name: "index_users_on_home_mid_id" + t.index ["home_return_address_id"], name: "index_users_on_home_return_address_id" + end + create_table "usps_indicia" do |t| + t.integer "processing_category" + t.float "postage_weight" + t.boolean "nonmachinable" + t.string "usps_sku" + t.decimal "postage" + t.date "mailing_date" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.bigint "usps_payment_account_id", null: false + t.bigint "letter_id" + t.jsonb "raw_json_response" + t.boolean "flirted" + t.decimal "fees" + t.index ["letter_id"], name: "index_usps_indicia_on_letter_id" + t.index ["usps_payment_account_id"], name: "index_usps_indicia_on_usps_payment_account_id" + end + create_table "usps_iv_mtr_events" do |t| + t.datetime "happened_at" + t.bigint "letter_id" + t.bigint "batch_id", null: false + t.jsonb "payload" + t.string "opcode" + t.string "zip_code" + t.bigint "mailer_id_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["batch_id"], name: "index_usps_iv_mtr_events_on_batch_id" + t.index ["letter_id"], name: "index_usps_iv_mtr_events_on_letter_id" + t.index ["mailer_id_id", "happened_at"], name: "index_usps_iv_mtr_events_on_mailer_id_id_and_happened_at" + t.index ["mailer_id_id", "opcode"], name: "index_usps_iv_mtr_events_on_mailer_id_id_and_opcode" + t.index ["mailer_id_id"], name: "index_usps_iv_mtr_events_on_mailer_id_id" + end + create_table "usps_iv_mtr_raw_json_batches" do |t| + t.jsonb "events" + t.boolean "processed" + t.string "message_group_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "usps_mailer_ids" do |t| + t.string "crid" + t.string "mid" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "name" + t.integer "rollover_count" + t.bigint "sequence_number" + end + create_table "usps_payment_accounts" do |t| + t.bigint "usps_mailer_id_id", null: false + t.integer "account_type" + t.string "account_number" + t.string "permit_number" + t.string "permit_zip" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "name" + t.string "manifest_mid" + t.boolean "ach" + t.index ["usps_mailer_id_id"], name: "index_usps_payment_accounts_on_usps_mailer_id_id" + end + create_table "warehouse_line_items" do |t| + t.integer "quantity" + t.bigint "sku_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.bigint "order_id" + t.bigint "template_id" + t.index ["order_id"], name: "index_warehouse_line_items_on_order_id" + t.index ["sku_id"], name: "index_warehouse_line_items_on_sku_id" + t.index ["template_id"], name: "index_warehouse_line_items_on_template_id" + end + create_table "warehouse_orders" do |t| + t.string "hc_id" + t.string "aasm_state" + t.string "recipient_email", null: false + t.bigint "user_id", null: false + t.boolean "surprise" + t.string "user_facing_title" + t.string "user_facing_description" + t.text "internal_notes" + t.integer "zenventory_id" + t.bigint "source_tag_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.bigint "address_id", null: false + t.datetime "dispatched_at" + t.datetime "mailed_at" + t.datetime "canceled_at" + t.string "carrier" + t.string "service" + t.string "tracking_number" + t.decimal "postage_cost" + t.decimal "weight" + t.string "idempotency_key" + t.boolean "notify_on_dispatch" + t.bigint "batch_id" + t.bigint "template_id" + t.jsonb "metadata" + t.citext "tags", default: [], array: true + t.decimal "labor_cost", precision: 10, scale: 2 + t.decimal "contents_cost", precision: 10, scale: 2 + t.index ["address_id"], name: "index_warehouse_orders_on_address_id" + t.index ["batch_id"], name: "index_warehouse_orders_on_batch_id" + t.index ["hc_id"], name: "index_warehouse_orders_on_hc_id" + t.index ["idempotency_key"], name: "index_warehouse_orders_on_idempotency_key", unique: true + t.index ["source_tag_id"], name: "index_warehouse_orders_on_source_tag_id" + t.index ["tags"], name: "index_warehouse_orders_on_tags", using: :gin + t.index ["template_id"], name: "index_warehouse_orders_on_template_id" + t.index ["user_id"], name: "index_warehouse_orders_on_user_id" + end + create_table "warehouse_skus" do |t| + t.string "sku" + t.text "description" + t.decimal "average_po_cost" + t.text "customs_description" + t.integer "in_stock" + t.boolean "ai_enabled" + t.boolean "enabled" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "hs_code" + t.string "country_of_origin" + t.integer "category" + t.string "name" + t.decimal "actual_cost_to_hc" + t.decimal "declared_unit_cost_override" + t.string "zenventory_id" + t.integer "inbound" + t.index ["sku"], name: "index_warehouse_skus_on_sku", unique: true + end + create_table "warehouse_templates" do |t| + t.bigint "user_id", null: false + t.string "name" + t.bigint "source_tag_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "public" + t.index ["source_tag_id"], name: "index_warehouse_templates_on_source_tag_id" + t.index ["user_id"], name: "index_warehouse_templates_on_user_id" + end + add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" + add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" + add_foreign_key "addresses", "batches" + add_foreign_key "api_keys", "users" + add_foreign_key "batches", "letter_queues" + add_foreign_key "batches", "return_addresses", column: "letter_return_address_id" + add_foreign_key "batches", "users" + add_foreign_key "batches", "usps_mailer_ids", column: "letter_mailer_id_id" + add_foreign_key "batches", "warehouse_templates" + add_foreign_key "letter_queues", "return_addresses", column: "letter_return_address_id" + add_foreign_key "letter_queues", "users" + add_foreign_key "letter_queues", "usps_mailer_ids", column: "letter_mailer_id_id" + add_foreign_key "letter_queues", "usps_payment_accounts" + add_foreign_key "letters", "addresses" + add_foreign_key "letters", "batches" + add_foreign_key "letters", "letter_queues" + add_foreign_key "letters", "return_addresses" + add_foreign_key "letters", "users" + add_foreign_key "letters", "usps_mailer_ids" + add_foreign_key "public_api_keys", "public_users" + add_foreign_key "public_impersonations", "users" + add_foreign_key "public_login_codes", "public_users", column: "user_id" + add_foreign_key "return_addresses", "users" + add_foreign_key "users", "return_addresses", column: "home_return_address_id" + add_foreign_key "users", "usps_mailer_ids", column: "home_mid_id" + add_foreign_key "usps_indicia", "letters" + add_foreign_key "usps_indicia", "usps_payment_accounts" + add_foreign_key "usps_iv_mtr_events", "letters", on_delete: :nullify + add_foreign_key "usps_iv_mtr_events", "usps_iv_mtr_raw_json_batches", column: "batch_id" + add_foreign_key "usps_iv_mtr_events", "usps_mailer_ids", column: "mailer_id_id" + add_foreign_key "usps_payment_accounts", "usps_mailer_ids" + add_foreign_key "warehouse_line_items", "warehouse_orders", column: "order_id" + add_foreign_key "warehouse_line_items", "warehouse_skus", column: "sku_id" + add_foreign_key "warehouse_line_items", "warehouse_templates", column: "template_id" + add_foreign_key "warehouse_orders", "addresses" + add_foreign_key "warehouse_orders", "batches" + add_foreign_key "warehouse_orders", "source_tags" + add_foreign_key "warehouse_orders", "users" + add_foreign_key "warehouse_orders", "warehouse_templates", column: "template_id" + add_foreign_key "warehouse_templates", "source_tags" + add_foreign_key "warehouse_templates", "users" + end + + def down + raise ActiveRecord::IrreversibleMigration, "The initial migration is not revertable" + end +end diff --git a/db/migrate/20250601023335_squasher_clean.rb b/db/migrate/20250601023335_squasher_clean.rb new file mode 100644 index 0000000..22d89c8 --- /dev/null +++ b/db/migrate/20250601023335_squasher_clean.rb @@ -0,0 +1,13 @@ +class SquasherClean < ActiveRecord::Migration[8.0] + class SchemaMigration < ActiveRecord::Base + end + + def up + migrations = Dir.glob(File.join(File.dirname(__FILE__), '*.rb')) + versions = migrations.map { |file| File.basename(file)[/\A\d+/] } + SchemaMigration.where("version NOT IN (?)", versions).delete_all + end + + def down + end +end diff --git a/db/queue_schema.rb b/db/queue_schema.rb new file mode 100644 index 0000000..85194b6 --- /dev/null +++ b/db/queue_schema.rb @@ -0,0 +1,129 @@ +ActiveRecord::Schema[7.1].define(version: 1) do + create_table "solid_queue_blocked_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.string "concurrency_key", null: false + t.datetime "expires_at", null: false + t.datetime "created_at", null: false + t.index [ "concurrency_key", "priority", "job_id" ], name: "index_solid_queue_blocked_executions_for_release" + t.index [ "expires_at", "concurrency_key" ], name: "index_solid_queue_blocked_executions_for_maintenance" + t.index [ "job_id" ], name: "index_solid_queue_blocked_executions_on_job_id", unique: true + end + + create_table "solid_queue_claimed_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.bigint "process_id" + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_claimed_executions_on_job_id", unique: true + t.index [ "process_id", "job_id" ], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id" + end + + create_table "solid_queue_failed_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.text "error" + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_failed_executions_on_job_id", unique: true + end + + create_table "solid_queue_jobs", force: :cascade do |t| + t.string "queue_name", null: false + t.string "class_name", null: false + t.text "arguments" + t.integer "priority", default: 0, null: false + t.string "active_job_id" + t.datetime "scheduled_at" + t.datetime "finished_at" + t.string "concurrency_key" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index [ "active_job_id" ], name: "index_solid_queue_jobs_on_active_job_id" + t.index [ "class_name" ], name: "index_solid_queue_jobs_on_class_name" + t.index [ "finished_at" ], name: "index_solid_queue_jobs_on_finished_at" + t.index [ "queue_name", "finished_at" ], name: "index_solid_queue_jobs_for_filtering" + t.index [ "scheduled_at", "finished_at" ], name: "index_solid_queue_jobs_for_alerting" + end + + create_table "solid_queue_pauses", force: :cascade do |t| + t.string "queue_name", null: false + t.datetime "created_at", null: false + t.index [ "queue_name" ], name: "index_solid_queue_pauses_on_queue_name", unique: true + end + + create_table "solid_queue_processes", force: :cascade do |t| + t.string "kind", null: false + t.datetime "last_heartbeat_at", null: false + t.bigint "supervisor_id" + t.integer "pid", null: false + t.string "hostname" + t.text "metadata" + t.datetime "created_at", null: false + t.string "name", null: false + t.index [ "last_heartbeat_at" ], name: "index_solid_queue_processes_on_last_heartbeat_at" + t.index [ "name", "supervisor_id" ], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true + t.index [ "supervisor_id" ], name: "index_solid_queue_processes_on_supervisor_id" + end + + create_table "solid_queue_ready_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_ready_executions_on_job_id", unique: true + t.index [ "priority", "job_id" ], name: "index_solid_queue_poll_all" + t.index [ "queue_name", "priority", "job_id" ], name: "index_solid_queue_poll_by_queue" + end + + create_table "solid_queue_recurring_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "task_key", null: false + t.datetime "run_at", null: false + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_recurring_executions_on_job_id", unique: true + t.index [ "task_key", "run_at" ], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true + end + + create_table "solid_queue_recurring_tasks", force: :cascade do |t| + t.string "key", null: false + t.string "schedule", null: false + t.string "command", limit: 2048 + t.string "class_name" + t.text "arguments" + t.string "queue_name" + t.integer "priority", default: 0 + t.boolean "static", default: true, null: false + t.text "description" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index [ "key" ], name: "index_solid_queue_recurring_tasks_on_key", unique: true + t.index [ "static" ], name: "index_solid_queue_recurring_tasks_on_static" + end + + create_table "solid_queue_scheduled_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.datetime "scheduled_at", null: false + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true + t.index [ "scheduled_at", "priority", "job_id" ], name: "index_solid_queue_dispatch_all" + end + + create_table "solid_queue_semaphores", force: :cascade do |t| + t.string "key", null: false + t.integer "value", default: 1, null: false + t.datetime "expires_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index [ "expires_at" ], name: "index_solid_queue_semaphores_on_expires_at" + t.index [ "key", "value" ], name: "index_solid_queue_semaphores_on_key_and_value" + t.index [ "key" ], name: "index_solid_queue_semaphores_on_key", unique: true + end + + add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000..d5a63be --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,607 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[8.0].define(version: 2025_06_01_023335) do + # These are extensions that must be enabled in order to support this database + enable_extension "citext" + enable_extension "pg_catalog.plpgsql" + enable_extension "pgcrypto" + + create_table "action_text_rich_texts", force: :cascade do |t| + t.string "name", null: false + t.text "body" + t.string "record_type", null: false + t.bigint "record_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true + end + + create_table "active_storage_attachments", force: :cascade do |t| + t.string "name", null: false + t.string "record_type", null: false + t.bigint "record_id", null: false + t.bigint "blob_id", null: false + t.datetime "created_at", null: false + t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" + t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true + end + + create_table "active_storage_blobs", force: :cascade do |t| + t.string "key", null: false + t.string "filename", null: false + t.string "content_type" + t.text "metadata" + t.string "service_name", null: false + t.bigint "byte_size", null: false + t.string "checksum" + t.datetime "created_at", null: false + t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true + end + + create_table "active_storage_variant_records", force: :cascade do |t| + t.bigint "blob_id", null: false + t.string "variation_digest", null: false + t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true + end + + create_table "addresses", force: :cascade do |t| + t.string "first_name" + t.string "last_name" + t.string "line_1" + t.string "line_2" + t.string "city" + t.string "state" + t.string "postal_code" + t.integer "country" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "phone_number" + t.bigint "batch_id" + t.string "email" + t.index ["batch_id"], name: "index_addresses_on_batch_id" + end + + create_table "api_keys", force: :cascade do |t| + t.bigint "user_id", null: false + t.datetime "revoked_at" + t.boolean "pii" + t.text "token_ciphertext" + t.string "token_bidx" + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "may_impersonate" + t.index ["token_bidx"], name: "index_api_keys_on_token_bidx", unique: true + t.index ["user_id"], name: "index_api_keys_on_user_id" + end + + create_table "batches", force: :cascade do |t| + t.bigint "user_id", null: false + t.jsonb "field_mapping" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "type", null: false + t.bigint "warehouse_template_id" + t.integer "address_count" + t.string "warehouse_user_facing_title" + t.string "aasm_state" + t.decimal "letter_height" + t.decimal "letter_width" + t.decimal "letter_weight" + t.bigint "letter_mailer_id_id" + t.bigint "letter_return_address_id" + t.citext "tags", default: [], array: true + t.integer "letter_processing_category" + t.date "letter_mailing_date" + t.string "template_cycle", default: [], array: true + t.string "letter_return_address_name" + t.bigint "letter_queue_id" + t.index ["letter_mailer_id_id"], name: "index_batches_on_letter_mailer_id_id" + t.index ["letter_queue_id"], name: "index_batches_on_letter_queue_id" + t.index ["letter_return_address_id"], name: "index_batches_on_letter_return_address_id" + t.index ["tags"], name: "index_batches_on_tags", using: :gin + t.index ["type"], name: "index_batches_on_type" + t.index ["user_id"], name: "index_batches_on_user_id" + t.index ["warehouse_template_id"], name: "index_batches_on_warehouse_template_id" + end + + create_table "blazer_audits", force: :cascade do |t| + t.bigint "user_id" + t.bigint "query_id" + t.text "statement" + t.string "data_source" + t.datetime "created_at" + t.index ["query_id"], name: "index_blazer_audits_on_query_id" + t.index ["user_id"], name: "index_blazer_audits_on_user_id" + end + + create_table "blazer_checks", force: :cascade do |t| + t.bigint "creator_id" + t.bigint "query_id" + t.string "state" + t.string "schedule" + t.text "emails" + t.text "slack_channels" + t.string "check_type" + t.text "message" + t.datetime "last_run_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["creator_id"], name: "index_blazer_checks_on_creator_id" + t.index ["query_id"], name: "index_blazer_checks_on_query_id" + end + + create_table "blazer_dashboard_queries", force: :cascade do |t| + t.bigint "dashboard_id" + t.bigint "query_id" + t.integer "position" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["dashboard_id"], name: "index_blazer_dashboard_queries_on_dashboard_id" + t.index ["query_id"], name: "index_blazer_dashboard_queries_on_query_id" + end + + create_table "blazer_dashboards", force: :cascade do |t| + t.bigint "creator_id" + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["creator_id"], name: "index_blazer_dashboards_on_creator_id" + end + + create_table "blazer_queries", force: :cascade do |t| + t.bigint "creator_id" + t.string "name" + t.text "description" + t.text "statement" + t.string "data_source" + t.string "status" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["creator_id"], name: "index_blazer_queries_on_creator_id" + end + + create_table "common_tags", force: :cascade do |t| + t.string "tag" + t.boolean "implies_ysws" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "good_job_batches", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.text "description" + t.jsonb "serialized_properties" + t.text "on_finish" + t.text "on_success" + t.text "on_discard" + t.text "callback_queue_name" + t.integer "callback_priority" + t.datetime "enqueued_at" + t.datetime "discarded_at" + t.datetime "finished_at" + t.datetime "jobs_finished_at" + end + + create_table "good_job_executions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.uuid "active_job_id", null: false + t.text "job_class" + t.text "queue_name" + t.jsonb "serialized_params" + t.datetime "scheduled_at" + t.datetime "finished_at" + t.text "error" + t.integer "error_event", limit: 2 + t.text "error_backtrace", array: true + t.uuid "process_id" + t.interval "duration" + t.index ["active_job_id", "created_at"], name: "index_good_job_executions_on_active_job_id_and_created_at" + t.index ["process_id", "created_at"], name: "index_good_job_executions_on_process_id_and_created_at" + end + + create_table "good_job_processes", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.jsonb "state" + t.integer "lock_type", limit: 2 + end + + create_table "good_job_settings", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.text "key" + t.jsonb "value" + t.index ["key"], name: "index_good_job_settings_on_key", unique: true + end + + create_table "good_jobs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.text "queue_name" + t.integer "priority" + t.jsonb "serialized_params" + t.datetime "scheduled_at" + t.datetime "performed_at" + t.datetime "finished_at" + t.text "error" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.uuid "active_job_id" + t.text "concurrency_key" + t.text "cron_key" + t.uuid "retried_good_job_id" + t.datetime "cron_at" + t.uuid "batch_id" + t.uuid "batch_callback_id" + t.boolean "is_discrete" + t.integer "executions_count" + t.text "job_class" + t.integer "error_event", limit: 2 + t.text "labels", array: true + t.uuid "locked_by_id" + t.datetime "locked_at" + t.index ["active_job_id", "created_at"], name: "index_good_jobs_on_active_job_id_and_created_at" + t.index ["batch_callback_id"], name: "index_good_jobs_on_batch_callback_id", where: "(batch_callback_id IS NOT NULL)" + t.index ["batch_id"], name: "index_good_jobs_on_batch_id", where: "(batch_id IS NOT NULL)" + t.index ["concurrency_key", "created_at"], name: "index_good_jobs_on_concurrency_key_and_created_at" + t.index ["concurrency_key"], name: "index_good_jobs_on_concurrency_key_when_unfinished", where: "(finished_at IS NULL)" + t.index ["cron_key", "created_at"], name: "index_good_jobs_on_cron_key_and_created_at_cond", where: "(cron_key IS NOT NULL)" + t.index ["cron_key", "cron_at"], name: "index_good_jobs_on_cron_key_and_cron_at_cond", unique: true, where: "(cron_key IS NOT NULL)" + t.index ["finished_at"], name: "index_good_jobs_jobs_on_finished_at", where: "((retried_good_job_id IS NULL) AND (finished_at IS NOT NULL))" + t.index ["labels"], name: "index_good_jobs_on_labels", where: "(labels IS NOT NULL)", using: :gin + t.index ["locked_by_id"], name: "index_good_jobs_on_locked_by_id", where: "(locked_by_id IS NOT NULL)" + t.index ["priority", "created_at"], name: "index_good_job_jobs_for_candidate_lookup", where: "(finished_at IS NULL)" + t.index ["priority", "created_at"], name: "index_good_jobs_jobs_on_priority_created_at_when_unfinished", order: { priority: "DESC NULLS LAST" }, where: "(finished_at IS NULL)" + t.index ["priority", "scheduled_at"], name: "index_good_jobs_on_priority_scheduled_at_unfinished_unlocked", where: "((finished_at IS NULL) AND (locked_by_id IS NULL))" + t.index ["queue_name", "scheduled_at"], name: "index_good_jobs_on_queue_name_and_scheduled_at", where: "(finished_at IS NULL)" + t.index ["scheduled_at"], name: "index_good_jobs_on_scheduled_at", where: "(finished_at IS NULL)" + end + + create_table "letter_queues", force: :cascade do |t| + t.string "name" + t.string "slug" + t.bigint "user_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.decimal "letter_height" + t.decimal "letter_width" + t.decimal "letter_weight" + t.integer "letter_processing_category" + t.bigint "letter_mailer_id_id" + t.bigint "letter_return_address_id" + t.string "letter_return_address_name" + t.string "user_facing_title" + t.citext "tags", default: [], array: true + t.string "type" + t.string "template" + t.string "postage_type" + t.bigint "usps_payment_account_id" + t.boolean "include_qr_code", default: true + t.date "letter_mailing_date" + t.index ["letter_mailer_id_id"], name: "index_letter_queues_on_letter_mailer_id_id" + t.index ["letter_return_address_id"], name: "index_letter_queues_on_letter_return_address_id" + t.index ["type"], name: "index_letter_queues_on_type" + t.index ["user_id"], name: "index_letter_queues_on_user_id" + end + + create_table "letters", force: :cascade do |t| + t.integer "processing_category" + t.text "body" + t.string "aasm_state" + t.bigint "usps_mailer_id_id", null: false + t.decimal "postage" + t.integer "imb_serial_number" + t.bigint "address_id", null: false + t.integer "imb_rollover_count" + t.string "recipient_email" + t.decimal "weight" + t.decimal "width" + t.decimal "height" + t.boolean "non_machinable" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.text "rubber_stamps" + t.bigint "batch_id" + t.bigint "return_address_id", null: false + t.jsonb "metadata" + t.citext "tags", default: [], array: true + t.integer "postage_type" + t.date "mailing_date" + t.datetime "printed_at" + t.datetime "mailed_at" + t.datetime "received_at" + t.string "user_facing_title" + t.bigint "user_id", null: false + t.string "return_address_name" + t.bigint "letter_queue_id" + t.string "idempotency_key" + t.index ["address_id"], name: "index_letters_on_address_id" + t.index ["batch_id"], name: "index_letters_on_batch_id" + t.index ["idempotency_key"], name: "index_letters_on_idempotency_key", unique: true + t.index ["imb_serial_number"], name: "index_letters_on_imb_serial_number" + t.index ["letter_queue_id"], name: "index_letters_on_letter_queue_id" + t.index ["return_address_id"], name: "index_letters_on_return_address_id" + t.index ["tags"], name: "index_letters_on_tags", using: :gin + t.index ["user_id"], name: "index_letters_on_user_id" + t.index ["usps_mailer_id_id"], name: "index_letters_on_usps_mailer_id_id" + end + + create_table "public_api_keys", force: :cascade do |t| + t.bigint "public_user_id", null: false + t.string "token_ciphertext" + t.datetime "revoked_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "name" + t.string "token_bidx" + t.index ["public_user_id"], name: "index_public_api_keys_on_public_user_id" + t.index ["token_bidx"], name: "index_public_api_keys_on_token_bidx", unique: true + end + + create_table "public_impersonations", force: :cascade do |t| + t.bigint "user_id", null: false + t.string "justification" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "target_email" + t.index ["user_id"], name: "index_public_impersonations_on_user_id" + end + + create_table "public_login_codes", force: :cascade do |t| + t.string "token" + t.datetime "expires_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.bigint "user_id", null: false + t.datetime "used_at" + t.index ["user_id"], name: "index_public_login_codes_on_user_id" + end + + create_table "public_users", force: :cascade do |t| + t.string "email" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "return_addresses", force: :cascade do |t| + t.string "name" + t.string "line_1" + t.string "line_2" + t.string "city" + t.string "state" + t.string "postal_code" + t.integer "country" + t.boolean "shared" + t.bigint "user_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_return_addresses_on_user_id" + end + + create_table "source_tags", force: :cascade do |t| + t.string "slug" + t.string "name" + t.string "owner" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "users", force: :cascade do |t| + t.string "slack_id" + t.string "email" + t.boolean "is_admin" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "icon_url" + t.string "username" + t.boolean "can_warehouse" + t.boolean "back_office", default: false + t.boolean "can_impersonate_public" + t.bigint "home_mid_id", default: 1, null: false + t.bigint "home_return_address_id", default: 1, null: false + t.index ["home_mid_id"], name: "index_users_on_home_mid_id" + t.index ["home_return_address_id"], name: "index_users_on_home_return_address_id" + end + + create_table "usps_indicia", force: :cascade do |t| + t.integer "processing_category" + t.float "postage_weight" + t.boolean "nonmachinable" + t.string "usps_sku" + t.decimal "postage" + t.date "mailing_date" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.bigint "usps_payment_account_id", null: false + t.bigint "letter_id" + t.jsonb "raw_json_response" + t.boolean "flirted" + t.decimal "fees" + t.index ["letter_id"], name: "index_usps_indicia_on_letter_id" + t.index ["usps_payment_account_id"], name: "index_usps_indicia_on_usps_payment_account_id" + end + + create_table "usps_iv_mtr_events", force: :cascade do |t| + t.datetime "happened_at" + t.bigint "letter_id" + t.bigint "batch_id", null: false + t.jsonb "payload" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "opcode" + t.string "zip_code" + t.bigint "mailer_id_id", null: false + t.index ["batch_id"], name: "index_usps_iv_mtr_events_on_batch_id" + t.index ["letter_id"], name: "index_usps_iv_mtr_events_on_letter_id" + t.index ["mailer_id_id"], name: "index_usps_iv_mtr_events_on_mailer_id_id" + end + + create_table "usps_iv_mtr_raw_json_batches", force: :cascade do |t| + t.jsonb "payload" + t.boolean "processed" + t.string "message_group_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "usps_mailer_ids", force: :cascade do |t| + t.string "crid" + t.string "mid" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "name" + t.integer "rollover_count" + t.bigint "sequence_number" + end + + create_table "usps_payment_accounts", force: :cascade do |t| + t.bigint "usps_mailer_id_id", null: false + t.integer "account_type" + t.string "account_number" + t.string "permit_number" + t.string "permit_zip" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "name" + t.string "manifest_mid" + t.boolean "ach" + t.index ["usps_mailer_id_id"], name: "index_usps_payment_accounts_on_usps_mailer_id_id" + end + + create_table "warehouse_line_items", force: :cascade do |t| + t.integer "quantity" + t.bigint "sku_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.bigint "order_id" + t.bigint "template_id" + t.index ["order_id"], name: "index_warehouse_line_items_on_order_id" + t.index ["sku_id"], name: "index_warehouse_line_items_on_sku_id" + t.index ["template_id"], name: "index_warehouse_line_items_on_template_id" + end + + create_table "warehouse_orders", force: :cascade do |t| + t.string "hc_id" + t.string "aasm_state" + t.string "recipient_email" + t.bigint "user_id", null: false + t.boolean "surprise" + t.string "user_facing_title" + t.string "user_facing_description" + t.text "internal_notes" + t.integer "zenventory_id" + t.bigint "source_tag_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.bigint "address_id", null: false + t.datetime "dispatched_at" + t.datetime "mailed_at" + t.datetime "canceled_at" + t.string "carrier" + t.string "service" + t.string "tracking_number" + t.decimal "postage_cost" + t.decimal "weight" + t.string "idempotency_key" + t.boolean "notify_on_dispatch" + t.bigint "batch_id" + t.bigint "template_id" + t.jsonb "metadata" + t.citext "tags", default: [], array: true + t.decimal "labor_cost", precision: 10, scale: 2 + t.decimal "contents_cost", precision: 10, scale: 2 + t.index ["address_id"], name: "index_warehouse_orders_on_address_id" + t.index ["batch_id"], name: "index_warehouse_orders_on_batch_id" + t.index ["hc_id"], name: "index_warehouse_orders_on_hc_id" + t.index ["idempotency_key"], name: "index_warehouse_orders_on_idempotency_key", unique: true + t.index ["source_tag_id"], name: "index_warehouse_orders_on_source_tag_id" + t.index ["tags"], name: "index_warehouse_orders_on_tags", using: :gin + t.index ["template_id"], name: "index_warehouse_orders_on_template_id" + t.index ["user_id"], name: "index_warehouse_orders_on_user_id" + end + + create_table "warehouse_skus", force: :cascade do |t| + t.string "sku" + t.text "description" + t.decimal "average_po_cost" + t.text "customs_description" + t.integer "in_stock" + t.boolean "ai_enabled" + t.boolean "enabled" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "hs_code" + t.string "country_of_origin" + t.integer "category" + t.string "name" + t.decimal "actual_cost_to_hc" + t.decimal "declared_unit_cost_override" + t.string "zenventory_id" + t.integer "inbound" + t.index ["sku"], name: "index_warehouse_skus_on_sku", unique: true + end + + create_table "warehouse_templates", force: :cascade do |t| + t.bigint "user_id", null: false + t.string "name" + t.bigint "source_tag_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "public" + t.index ["source_tag_id"], name: "index_warehouse_templates_on_source_tag_id" + t.index ["user_id"], name: "index_warehouse_templates_on_user_id" + end + + add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" + add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" + add_foreign_key "addresses", "batches" + add_foreign_key "api_keys", "users" + add_foreign_key "batches", "letter_queues" + add_foreign_key "batches", "return_addresses", column: "letter_return_address_id" + add_foreign_key "batches", "users" + add_foreign_key "batches", "usps_mailer_ids", column: "letter_mailer_id_id" + add_foreign_key "batches", "warehouse_templates" + add_foreign_key "letter_queues", "return_addresses", column: "letter_return_address_id" + add_foreign_key "letter_queues", "users" + add_foreign_key "letter_queues", "usps_mailer_ids", column: "letter_mailer_id_id" + add_foreign_key "letter_queues", "usps_payment_accounts" + add_foreign_key "letters", "addresses" + add_foreign_key "letters", "batches" + add_foreign_key "letters", "letter_queues" + add_foreign_key "letters", "return_addresses" + add_foreign_key "letters", "users" + add_foreign_key "letters", "usps_mailer_ids" + add_foreign_key "public_api_keys", "public_users" + add_foreign_key "public_impersonations", "users" + add_foreign_key "public_login_codes", "public_users", column: "user_id" + add_foreign_key "return_addresses", "users" + add_foreign_key "users", "return_addresses", column: "home_return_address_id" + add_foreign_key "users", "usps_mailer_ids", column: "home_mid_id" + add_foreign_key "usps_indicia", "letters" + add_foreign_key "usps_indicia", "usps_payment_accounts" + add_foreign_key "usps_iv_mtr_events", "letters", on_delete: :nullify + add_foreign_key "usps_iv_mtr_events", "usps_iv_mtr_raw_json_batches", column: "batch_id" + add_foreign_key "usps_iv_mtr_events", "usps_mailer_ids", column: "mailer_id_id" + add_foreign_key "usps_payment_accounts", "usps_mailer_ids" + add_foreign_key "warehouse_line_items", "warehouse_orders", column: "order_id" + add_foreign_key "warehouse_line_items", "warehouse_skus", column: "sku_id" + add_foreign_key "warehouse_line_items", "warehouse_templates", column: "template_id" + add_foreign_key "warehouse_orders", "addresses" + add_foreign_key "warehouse_orders", "batches" + add_foreign_key "warehouse_orders", "source_tags" + add_foreign_key "warehouse_orders", "users" + add_foreign_key "warehouse_orders", "warehouse_templates", column: "template_id" + add_foreign_key "warehouse_templates", "source_tags" + add_foreign_key "warehouse_templates", "users" +end diff --git a/db/seeds.rb b/db/seeds.rb new file mode 100644 index 0000000..b1e25c1 --- /dev/null +++ b/db/seeds.rb @@ -0,0 +1,19 @@ +# This file should ensure the existence of records required to run the application in every environment (production, +# development, test). The code here should be idempotent so that it can be executed at any point in every environment. +# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). +# +# Example: +# +# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| +# MovieGenre.find_or_create_by!(name: genre_name) +# end +SourceTag.find_or_create_by!( + name: "Theseus web interface", + slug: "theseus_web", + owner: "Nora" +) + +Warehouse::PurposeCode.find_or_create_by!( + code: Rails.env.production? ? 'HQ' : 'HQ-dev', + description: 'general HQ mailing' +) diff --git a/docker-compose-dbonly.yml b/docker-compose-dbonly.yml new file mode 100644 index 0000000..1fc95d9 --- /dev/null +++ b/docker-compose-dbonly.yml @@ -0,0 +1,21 @@ +# To use this DB-only ("dockerless") setup, pass a `-f docker-compose.dbonly.yml` to docker compose. +# e.g. `docker compose -f docker-compose.dbonly.yml up` +services: + db: + image: "postgres:11.16" + volumes: + - pg-data:/var/lib/postgresql/data + environment: + POSTGRES_HOST_AUTH_METHOD: trust + ports: + - 5432:5432 + redis: + image: redis + volumes: + - redis-data:/data + ports: + - 6379:6379 + +volumes: + pg-data: + redis-data: diff --git a/lib/tasks/generate_sm_previews.rake b/lib/tasks/generate_sm_previews.rake new file mode 100644 index 0000000..9cbddc8 --- /dev/null +++ b/lib/tasks/generate_sm_previews.rake @@ -0,0 +1,7 @@ +namespace :assets do + desc "generate letter template previews" + task generate_sm_previews: :environment do + SnailMail::Preview.generate_previews + end + Rake::Task["assets:precompile"].enhance(["assets:generate_sm_previews"]) +end diff --git a/log/.keep b/log/.keep new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json new file mode 100644 index 0000000..0046585 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "app", + "private": true, + "devDependencies": { + "@rollup/plugin-node-resolve": "^16.0.0", + "rollup": "^4.34.9", + "sass-embedded": "^1.86.3", + "vite": "^5.0.0", + "vite-plugin-ruby": "^5.1.0" + }, + "dependencies": { + "98.css": "^0.1.21", + "@hotwired/stimulus": "^3.2.2", + "@hotwired/turbo-rails": "^8.0.13", + "@nathanvda/cocoon": "^1.2.14", + "@oddcamp/cocoon-vanilla-js": "^1.1.3", + "@open-iframe-resizer/core": "^1.4.3", + "@picocss/pico": "^2.1.1", + "@selectize/selectize": "^0.15.2", + "@tsparticles/confetti": "^3.8.1", + "d3": "^7.9.0", + "datamaps": "^0.5.9", + "dreamland": "^0.0.25", + "jquery": "^3.7.1", + "qz-tray": "^2.2.4", + "select2": "^4.1.0-rc.0", + "selectize": "^0.12.6", + "topojson": "^3.0.2", + "vanilla-nested": "^1.7.1", + "vite-plugin-rails": "^0.5.0" + } +} diff --git a/public/.vite/manifest-assets.json b/public/.vite/manifest-assets.json new file mode 100644 index 0000000..8b39a4d --- /dev/null +++ b/public/.vite/manifest-assets.json @@ -0,0 +1,6 @@ +{ + "images/login/treasure.png": { + "file": "assets/treasure-KPG0amXu.png", + "src": "images/login/treasure.png" + } +} \ No newline at end of file diff --git a/public/.vite/manifest.json b/public/.vite/manifest.json new file mode 100644 index 0000000..19062c2 --- /dev/null +++ b/public/.vite/manifest.json @@ -0,0 +1,22 @@ +{ + "entrypoints/app_style.scss": { + "file": "assets/app_style-DayGOoOA.css", + "src": "entrypoints/app_style.scss", + "isEntry": true + }, + "entrypoints/cocoon.js": { + "file": "assets/cocoon-D1uP02It.js", + "name": "entrypoints/cocoon.js", + "src": "entrypoints/cocoon.js", + "isEntry": true + }, + "entrypoints/login_page.scss": { + "file": "assets/login_page-C8iuognS.css", + "src": "entrypoints/login_page.scss", + "isEntry": true + }, + "treasure.png": { + "file": "assets/treasure-KPG0amXu.png", + "src": "treasure.png" + } +} \ No newline at end of file diff --git a/public/500.html b/public/500.html new file mode 100644 index 0000000..55eb910 --- /dev/null +++ b/public/500.html @@ -0,0 +1,110 @@ + + + + + + + error!! + + + + +
+
+ +
+

ERROR 500

+

+ An internal server error has occurred.
+ Please poke @nora with the + following error ID and what you were trying to do when this happened. +

+

+ Error ID: + (click to copy) +

+ <- return to homepage +
+ + + \ No newline at end of file diff --git a/public/map_js/d3.v3.min.js b/public/map_js/d3.v3.min.js new file mode 100644 index 0000000..1664873 --- /dev/null +++ b/public/map_js/d3.v3.min.js @@ -0,0 +1,5 @@ +!function(){function n(n){return n&&(n.ownerDocument||n.document||n).documentElement}function t(n){return n&&(n.ownerDocument&&n.ownerDocument.defaultView||n.document&&n||n.defaultView)}function e(n,t){return t>n?-1:n>t?1:n>=t?0:NaN}function r(n){return null===n?NaN:+n}function i(n){return!isNaN(n)}function u(n){return{left:function(t,e,r,i){for(arguments.length<3&&(r=0),arguments.length<4&&(i=t.length);i>r;){var u=r+i>>>1;n(t[u],e)<0?r=u+1:i=u}return r},right:function(t,e,r,i){for(arguments.length<3&&(r=0),arguments.length<4&&(i=t.length);i>r;){var u=r+i>>>1;n(t[u],e)>0?i=u:r=u+1}return r}}}function o(n){return n.length}function a(n){for(var t=1;n*t%1;)t*=10;return t}function l(n,t){for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}function c(){this._=Object.create(null)}function f(n){return(n+="")===bo||n[0]===_o?_o+n:n}function s(n){return(n+="")[0]===_o?n.slice(1):n}function h(n){return f(n)in this._}function p(n){return(n=f(n))in this._&&delete this._[n]}function g(){var n=[];for(var t in this._)n.push(s(t));return n}function v(){var n=0;for(var t in this._)++n;return n}function d(){for(var n in this._)return!1;return!0}function y(){this._=Object.create(null)}function m(n){return n}function M(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function x(n,t){if(t in n)return t;t=t.charAt(0).toUpperCase()+t.slice(1);for(var e=0,r=wo.length;r>e;++e){var i=wo[e]+t;if(i in n)return i}}function b(){}function _(){}function w(n){function t(){for(var t,r=e,i=-1,u=r.length;++ie;e++)for(var i,u=n[e],o=0,a=u.length;a>o;o++)(i=u[o])&&t(i,o,e);return n}function Z(n){return ko(n,qo),n}function V(n){var t,e;return function(r,i,u){var o,a=n[u].update,l=a.length;for(u!=e&&(e=u,t=0),i>=t&&(t=i+1);!(o=a[t])&&++t0&&(n=n.slice(0,a));var c=To.get(n);return c&&(n=c,l=B),a?t?i:r:t?b:u}function $(n,t){return function(e){var r=ao.event;ao.event=e,t[0]=this.__data__;try{n.apply(this,t)}finally{ao.event=r}}}function B(n,t){var e=$(n,t);return function(n){var t=this,r=n.relatedTarget;r&&(r===t||8&r.compareDocumentPosition(t))||e.call(t,n)}}function W(e){var r=".dragsuppress-"+ ++Do,i="click"+r,u=ao.select(t(e)).on("touchmove"+r,S).on("dragstart"+r,S).on("selectstart"+r,S);if(null==Ro&&(Ro="onselectstart"in e?!1:x(e.style,"userSelect")),Ro){var o=n(e).style,a=o[Ro];o[Ro]="none"}return function(n){if(u.on(r,null),Ro&&(o[Ro]=a),n){var t=function(){u.on(i,null)};u.on(i,function(){S(),t()},!0),setTimeout(t,0)}}}function J(n,e){e.changedTouches&&(e=e.changedTouches[0]);var r=n.ownerSVGElement||n;if(r.createSVGPoint){var i=r.createSVGPoint();if(0>Po){var u=t(n);if(u.scrollX||u.scrollY){r=ao.select("body").append("svg").style({position:"absolute",top:0,left:0,margin:0,padding:0,border:"none"},"important");var o=r[0][0].getScreenCTM();Po=!(o.f||o.e),r.remove()}}return Po?(i.x=e.pageX,i.y=e.pageY):(i.x=e.clientX,i.y=e.clientY),i=i.matrixTransform(n.getScreenCTM().inverse()),[i.x,i.y]}var a=n.getBoundingClientRect();return[e.clientX-a.left-n.clientLeft,e.clientY-a.top-n.clientTop]}function G(){return ao.event.changedTouches[0].identifier}function K(n){return n>0?1:0>n?-1:0}function Q(n,t,e){return(t[0]-n[0])*(e[1]-n[1])-(t[1]-n[1])*(e[0]-n[0])}function nn(n){return n>1?0:-1>n?Fo:Math.acos(n)}function tn(n){return n>1?Io:-1>n?-Io:Math.asin(n)}function en(n){return((n=Math.exp(n))-1/n)/2}function rn(n){return((n=Math.exp(n))+1/n)/2}function un(n){return((n=Math.exp(2*n))-1)/(n+1)}function on(n){return(n=Math.sin(n/2))*n}function an(){}function ln(n,t,e){return this instanceof ln?(this.h=+n,this.s=+t,void(this.l=+e)):arguments.length<2?n instanceof ln?new ln(n.h,n.s,n.l):_n(""+n,wn,ln):new ln(n,t,e)}function cn(n,t,e){function r(n){return n>360?n-=360:0>n&&(n+=360),60>n?u+(o-u)*n/60:180>n?o:240>n?u+(o-u)*(240-n)/60:u}function i(n){return Math.round(255*r(n))}var u,o;return n=isNaN(n)?0:(n%=360)<0?n+360:n,t=isNaN(t)?0:0>t?0:t>1?1:t,e=0>e?0:e>1?1:e,o=.5>=e?e*(1+t):e+t-e*t,u=2*e-o,new mn(i(n+120),i(n),i(n-120))}function fn(n,t,e){return this instanceof fn?(this.h=+n,this.c=+t,void(this.l=+e)):arguments.length<2?n instanceof fn?new fn(n.h,n.c,n.l):n instanceof hn?gn(n.l,n.a,n.b):gn((n=Sn((n=ao.rgb(n)).r,n.g,n.b)).l,n.a,n.b):new fn(n,t,e)}function sn(n,t,e){return isNaN(n)&&(n=0),isNaN(t)&&(t=0),new hn(e,Math.cos(n*=Yo)*t,Math.sin(n)*t)}function hn(n,t,e){return this instanceof hn?(this.l=+n,this.a=+t,void(this.b=+e)):arguments.length<2?n instanceof hn?new hn(n.l,n.a,n.b):n instanceof fn?sn(n.h,n.c,n.l):Sn((n=mn(n)).r,n.g,n.b):new hn(n,t,e)}function pn(n,t,e){var r=(n+16)/116,i=r+t/500,u=r-e/200;return i=vn(i)*na,r=vn(r)*ta,u=vn(u)*ea,new mn(yn(3.2404542*i-1.5371385*r-.4985314*u),yn(-.969266*i+1.8760108*r+.041556*u),yn(.0556434*i-.2040259*r+1.0572252*u))}function gn(n,t,e){return n>0?new fn(Math.atan2(e,t)*Zo,Math.sqrt(t*t+e*e),n):new fn(NaN,NaN,n)}function vn(n){return n>.206893034?n*n*n:(n-4/29)/7.787037}function dn(n){return n>.008856?Math.pow(n,1/3):7.787037*n+4/29}function yn(n){return Math.round(255*(.00304>=n?12.92*n:1.055*Math.pow(n,1/2.4)-.055))}function mn(n,t,e){return this instanceof mn?(this.r=~~n,this.g=~~t,void(this.b=~~e)):arguments.length<2?n instanceof mn?new mn(n.r,n.g,n.b):_n(""+n,mn,cn):new mn(n,t,e)}function Mn(n){return new mn(n>>16,n>>8&255,255&n)}function xn(n){return Mn(n)+""}function bn(n){return 16>n?"0"+Math.max(0,n).toString(16):Math.min(255,n).toString(16)}function _n(n,t,e){var r,i,u,o=0,a=0,l=0;if(r=/([a-z]+)\((.*)\)/.exec(n=n.toLowerCase()))switch(i=r[2].split(","),r[1]){case"hsl":return e(parseFloat(i[0]),parseFloat(i[1])/100,parseFloat(i[2])/100);case"rgb":return t(Nn(i[0]),Nn(i[1]),Nn(i[2]))}return(u=ua.get(n))?t(u.r,u.g,u.b):(null==n||"#"!==n.charAt(0)||isNaN(u=parseInt(n.slice(1),16))||(4===n.length?(o=(3840&u)>>4,o=o>>4|o,a=240&u,a=a>>4|a,l=15&u,l=l<<4|l):7===n.length&&(o=(16711680&u)>>16,a=(65280&u)>>8,l=255&u)),t(o,a,l))}function wn(n,t,e){var r,i,u=Math.min(n/=255,t/=255,e/=255),o=Math.max(n,t,e),a=o-u,l=(o+u)/2;return a?(i=.5>l?a/(o+u):a/(2-o-u),r=n==o?(t-e)/a+(e>t?6:0):t==o?(e-n)/a+2:(n-t)/a+4,r*=60):(r=NaN,i=l>0&&1>l?0:r),new ln(r,i,l)}function Sn(n,t,e){n=kn(n),t=kn(t),e=kn(e);var r=dn((.4124564*n+.3575761*t+.1804375*e)/na),i=dn((.2126729*n+.7151522*t+.072175*e)/ta),u=dn((.0193339*n+.119192*t+.9503041*e)/ea);return hn(116*i-16,500*(r-i),200*(i-u))}function kn(n){return(n/=255)<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4)}function Nn(n){var t=parseFloat(n);return"%"===n.charAt(n.length-1)?Math.round(2.55*t):t}function En(n){return"function"==typeof n?n:function(){return n}}function An(n){return function(t,e,r){return 2===arguments.length&&"function"==typeof e&&(r=e,e=null),Cn(t,e,n,r)}}function Cn(n,t,e,r){function i(){var n,t=l.status;if(!t&&Ln(l)||t>=200&&300>t||304===t){try{n=e.call(u,l)}catch(r){return void o.error.call(u,r)}o.load.call(u,n)}else o.error.call(u,l)}var u={},o=ao.dispatch("beforesend","progress","load","error"),a={},l=new XMLHttpRequest,c=null;return!this.XDomainRequest||"withCredentials"in l||!/^(http(s)?:)?\/\//.test(n)||(l=new XDomainRequest),"onload"in l?l.onload=l.onerror=i:l.onreadystatechange=function(){l.readyState>3&&i()},l.onprogress=function(n){var t=ao.event;ao.event=n;try{o.progress.call(u,l)}finally{ao.event=t}},u.header=function(n,t){return n=(n+"").toLowerCase(),arguments.length<2?a[n]:(null==t?delete a[n]:a[n]=t+"",u)},u.mimeType=function(n){return arguments.length?(t=null==n?null:n+"",u):t},u.responseType=function(n){return arguments.length?(c=n,u):c},u.response=function(n){return e=n,u},["get","post"].forEach(function(n){u[n]=function(){return u.send.apply(u,[n].concat(co(arguments)))}}),u.send=function(e,r,i){if(2===arguments.length&&"function"==typeof r&&(i=r,r=null),l.open(e,n,!0),null==t||"accept"in a||(a.accept=t+",*/*"),l.setRequestHeader)for(var f in a)l.setRequestHeader(f,a[f]);return null!=t&&l.overrideMimeType&&l.overrideMimeType(t),null!=c&&(l.responseType=c),null!=i&&u.on("error",i).on("load",function(n){i(null,n)}),o.beforesend.call(u,l),l.send(null==r?null:r),u},u.abort=function(){return l.abort(),u},ao.rebind(u,o,"on"),null==r?u:u.get(zn(r))}function zn(n){return 1===n.length?function(t,e){n(null==t?e:null)}:n}function Ln(n){var t=n.responseType;return t&&"text"!==t?n.response:n.responseText}function qn(n,t,e){var r=arguments.length;2>r&&(t=0),3>r&&(e=Date.now());var i=e+t,u={c:n,t:i,n:null};return aa?aa.n=u:oa=u,aa=u,la||(ca=clearTimeout(ca),la=1,fa(Tn)),u}function Tn(){var n=Rn(),t=Dn()-n;t>24?(isFinite(t)&&(clearTimeout(ca),ca=setTimeout(Tn,t)),la=0):(la=1,fa(Tn))}function Rn(){for(var n=Date.now(),t=oa;t;)n>=t.t&&t.c(n-t.t)&&(t.c=null),t=t.n;return n}function Dn(){for(var n,t=oa,e=1/0;t;)t.c?(t.t8?function(n){return n/e}:function(n){return n*e},symbol:n}}function jn(n){var t=n.decimal,e=n.thousands,r=n.grouping,i=n.currency,u=r&&e?function(n,t){for(var i=n.length,u=[],o=0,a=r[0],l=0;i>0&&a>0&&(l+a+1>t&&(a=Math.max(1,t-l)),u.push(n.substring(i-=a,i+a)),!((l+=a+1)>t));)a=r[o=(o+1)%r.length];return u.reverse().join(e)}:m;return function(n){var e=ha.exec(n),r=e[1]||" ",o=e[2]||">",a=e[3]||"-",l=e[4]||"",c=e[5],f=+e[6],s=e[7],h=e[8],p=e[9],g=1,v="",d="",y=!1,m=!0;switch(h&&(h=+h.substring(1)),(c||"0"===r&&"="===o)&&(c=r="0",o="="),p){case"n":s=!0,p="g";break;case"%":g=100,d="%",p="f";break;case"p":g=100,d="%",p="r";break;case"b":case"o":case"x":case"X":"#"===l&&(v="0"+p.toLowerCase());case"c":m=!1;case"d":y=!0,h=0;break;case"s":g=-1,p="r"}"$"===l&&(v=i[0],d=i[1]),"r"!=p||h||(p="g"),null!=h&&("g"==p?h=Math.max(1,Math.min(21,h)):"e"!=p&&"f"!=p||(h=Math.max(0,Math.min(20,h)))),p=pa.get(p)||Fn;var M=c&&s;return function(n){var e=d;if(y&&n%1)return"";var i=0>n||0===n&&0>1/n?(n=-n,"-"):"-"===a?"":a;if(0>g){var l=ao.formatPrefix(n,h);n=l.scale(n),e=l.symbol+d}else n*=g;n=p(n,h);var x,b,_=n.lastIndexOf(".");if(0>_){var w=m?n.lastIndexOf("e"):-1;0>w?(x=n,b=""):(x=n.substring(0,w),b=n.substring(w))}else x=n.substring(0,_),b=t+n.substring(_+1);!c&&s&&(x=u(x,1/0));var S=v.length+x.length+b.length+(M?0:i.length),k=f>S?new Array(S=f-S+1).join(r):"";return M&&(x=u(k+x,k.length?f-b.length:1/0)),i+=v,n=x+b,("<"===o?i+n+k:">"===o?k+i+n:"^"===o?k.substring(0,S>>=1)+i+n+k.substring(S):i+(M?n:k+n))+e}}}function Fn(n){return n+""}function Hn(){this._=new Date(arguments.length>1?Date.UTC.apply(this,arguments):arguments[0])}function On(n,t,e){function r(t){var e=n(t),r=u(e,1);return r-t>t-e?e:r}function i(e){return t(e=n(new va(e-1)),1),e}function u(n,e){return t(n=new va(+n),e),n}function o(n,r,u){var o=i(n),a=[];if(u>1)for(;r>o;)e(o)%u||a.push(new Date(+o)),t(o,1);else for(;r>o;)a.push(new Date(+o)),t(o,1);return a}function a(n,t,e){try{va=Hn;var r=new Hn;return r._=n,o(r,t,e)}finally{va=Date}}n.floor=n,n.round=r,n.ceil=i,n.offset=u,n.range=o;var l=n.utc=In(n);return l.floor=l,l.round=In(r),l.ceil=In(i),l.offset=In(u),l.range=a,n}function In(n){return function(t,e){try{va=Hn;var r=new Hn;return r._=t,n(r,e)._}finally{va=Date}}}function Yn(n){function t(n){function t(t){for(var e,i,u,o=[],a=-1,l=0;++aa;){if(r>=c)return-1;if(i=t.charCodeAt(a++),37===i){if(o=t.charAt(a++),u=C[o in ya?t.charAt(a++):o],!u||(r=u(n,e,r))<0)return-1}else if(i!=e.charCodeAt(r++))return-1}return r}function r(n,t,e){_.lastIndex=0;var r=_.exec(t.slice(e));return r?(n.w=w.get(r[0].toLowerCase()),e+r[0].length):-1}function i(n,t,e){x.lastIndex=0;var r=x.exec(t.slice(e));return r?(n.w=b.get(r[0].toLowerCase()),e+r[0].length):-1}function u(n,t,e){N.lastIndex=0;var r=N.exec(t.slice(e));return r?(n.m=E.get(r[0].toLowerCase()),e+r[0].length):-1}function o(n,t,e){S.lastIndex=0;var r=S.exec(t.slice(e));return r?(n.m=k.get(r[0].toLowerCase()),e+r[0].length):-1}function a(n,t,r){return e(n,A.c.toString(),t,r)}function l(n,t,r){return e(n,A.x.toString(),t,r)}function c(n,t,r){return e(n,A.X.toString(),t,r)}function f(n,t,e){var r=M.get(t.slice(e,e+=2).toLowerCase());return null==r?-1:(n.p=r,e)}var s=n.dateTime,h=n.date,p=n.time,g=n.periods,v=n.days,d=n.shortDays,y=n.months,m=n.shortMonths;t.utc=function(n){function e(n){try{va=Hn;var t=new va;return t._=n,r(t)}finally{va=Date}}var r=t(n);return e.parse=function(n){try{va=Hn;var t=r.parse(n);return t&&t._}finally{va=Date}},e.toString=r.toString,e},t.multi=t.utc.multi=ct;var M=ao.map(),x=Vn(v),b=Xn(v),_=Vn(d),w=Xn(d),S=Vn(y),k=Xn(y),N=Vn(m),E=Xn(m);g.forEach(function(n,t){M.set(n.toLowerCase(),t)});var A={a:function(n){return d[n.getDay()]},A:function(n){return v[n.getDay()]},b:function(n){return m[n.getMonth()]},B:function(n){return y[n.getMonth()]},c:t(s),d:function(n,t){return Zn(n.getDate(),t,2)},e:function(n,t){return Zn(n.getDate(),t,2)},H:function(n,t){return Zn(n.getHours(),t,2)},I:function(n,t){return Zn(n.getHours()%12||12,t,2)},j:function(n,t){return Zn(1+ga.dayOfYear(n),t,3)},L:function(n,t){return Zn(n.getMilliseconds(),t,3)},m:function(n,t){return Zn(n.getMonth()+1,t,2)},M:function(n,t){return Zn(n.getMinutes(),t,2)},p:function(n){return g[+(n.getHours()>=12)]},S:function(n,t){return Zn(n.getSeconds(),t,2)},U:function(n,t){return Zn(ga.sundayOfYear(n),t,2)},w:function(n){return n.getDay()},W:function(n,t){return Zn(ga.mondayOfYear(n),t,2)},x:t(h),X:t(p),y:function(n,t){return Zn(n.getFullYear()%100,t,2)},Y:function(n,t){return Zn(n.getFullYear()%1e4,t,4)},Z:at,"%":function(){return"%"}},C={a:r,A:i,b:u,B:o,c:a,d:tt,e:tt,H:rt,I:rt,j:et,L:ot,m:nt,M:it,p:f,S:ut,U:Bn,w:$n,W:Wn,x:l,X:c,y:Gn,Y:Jn,Z:Kn,"%":lt};return t}function Zn(n,t,e){var r=0>n?"-":"",i=(r?-n:n)+"",u=i.length;return r+(e>u?new Array(e-u+1).join(t)+i:i)}function Vn(n){return new RegExp("^(?:"+n.map(ao.requote).join("|")+")","i")}function Xn(n){for(var t=new c,e=-1,r=n.length;++e68?1900:2e3)}function nt(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.m=r[0]-1,e+r[0].length):-1}function tt(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.d=+r[0],e+r[0].length):-1}function et(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+3));return r?(n.j=+r[0],e+r[0].length):-1}function rt(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.H=+r[0],e+r[0].length):-1}function it(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.M=+r[0],e+r[0].length):-1}function ut(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.S=+r[0],e+r[0].length):-1}function ot(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+3));return r?(n.L=+r[0],e+r[0].length):-1}function at(n){var t=n.getTimezoneOffset(),e=t>0?"-":"+",r=xo(t)/60|0,i=xo(t)%60;return e+Zn(r,"0",2)+Zn(i,"0",2)}function lt(n,t,e){Ma.lastIndex=0;var r=Ma.exec(t.slice(e,e+1));return r?e+r[0].length:-1}function ct(n){for(var t=n.length,e=-1;++e=0?1:-1,a=o*e,l=Math.cos(t),c=Math.sin(t),f=u*c,s=i*l+f*Math.cos(a),h=f*o*Math.sin(a);ka.add(Math.atan2(h,s)),r=n,i=l,u=c}var t,e,r,i,u;Na.point=function(o,a){Na.point=n,r=(t=o)*Yo,i=Math.cos(a=(e=a)*Yo/2+Fo/4),u=Math.sin(a)},Na.lineEnd=function(){n(t,e)}}function dt(n){var t=n[0],e=n[1],r=Math.cos(e);return[r*Math.cos(t),r*Math.sin(t),Math.sin(e)]}function yt(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]}function mt(n,t){return[n[1]*t[2]-n[2]*t[1],n[2]*t[0]-n[0]*t[2],n[0]*t[1]-n[1]*t[0]]}function Mt(n,t){n[0]+=t[0],n[1]+=t[1],n[2]+=t[2]}function xt(n,t){return[n[0]*t,n[1]*t,n[2]*t]}function bt(n){var t=Math.sqrt(n[0]*n[0]+n[1]*n[1]+n[2]*n[2]);n[0]/=t,n[1]/=t,n[2]/=t}function _t(n){return[Math.atan2(n[1],n[0]),tn(n[2])]}function wt(n,t){return xo(n[0]-t[0])a;++a)i.point((e=n[a])[0],e[1]);return void i.lineEnd()}var l=new Tt(e,n,null,!0),c=new Tt(e,null,l,!1);l.o=c,u.push(l),o.push(c),l=new Tt(r,n,null,!1),c=new Tt(r,null,l,!0),l.o=c,u.push(l),o.push(c)}}),o.sort(t),qt(u),qt(o),u.length){for(var a=0,l=e,c=o.length;c>a;++a)o[a].e=l=!l;for(var f,s,h=u[0];;){for(var p=h,g=!0;p.v;)if((p=p.n)===h)return;f=p.z,i.lineStart();do{if(p.v=p.o.v=!0,p.e){if(g)for(var a=0,c=f.length;c>a;++a)i.point((s=f[a])[0],s[1]);else r(p.x,p.n.x,1,i);p=p.n}else{if(g){f=p.p.z;for(var a=f.length-1;a>=0;--a)i.point((s=f[a])[0],s[1])}else r(p.x,p.p.x,-1,i);p=p.p}p=p.o,f=p.z,g=!g}while(!p.v);i.lineEnd()}}}function qt(n){if(t=n.length){for(var t,e,r=0,i=n[0];++r0){for(b||(u.polygonStart(),b=!0),u.lineStart();++o1&&2&t&&e.push(e.pop().concat(e.shift())),p.push(e.filter(Dt))}var p,g,v,d=t(u),y=i.invert(r[0],r[1]),m={point:o,lineStart:l,lineEnd:c,polygonStart:function(){m.point=f,m.lineStart=s,m.lineEnd=h,p=[],g=[]},polygonEnd:function(){m.point=o,m.lineStart=l,m.lineEnd=c,p=ao.merge(p);var n=Ot(y,g);p.length?(b||(u.polygonStart(),b=!0),Lt(p,Ut,n,e,u)):n&&(b||(u.polygonStart(),b=!0),u.lineStart(),e(null,null,1,u),u.lineEnd()),b&&(u.polygonEnd(),b=!1),p=g=null},sphere:function(){u.polygonStart(),u.lineStart(),e(null,null,1,u),u.lineEnd(),u.polygonEnd()}},M=Pt(),x=t(M),b=!1;return m}}function Dt(n){return n.length>1}function Pt(){var n,t=[];return{lineStart:function(){t.push(n=[])},point:function(t,e){n.push([t,e])},lineEnd:b,buffer:function(){var e=t;return t=[],n=null,e},rejoin:function(){t.length>1&&t.push(t.pop().concat(t.shift()))}}}function Ut(n,t){return((n=n.x)[0]<0?n[1]-Io-Uo:Io-n[1])-((t=t.x)[0]<0?t[1]-Io-Uo:Io-t[1])}function jt(n){var t,e=NaN,r=NaN,i=NaN;return{lineStart:function(){n.lineStart(),t=1},point:function(u,o){var a=u>0?Fo:-Fo,l=xo(u-e);xo(l-Fo)0?Io:-Io),n.point(i,r),n.lineEnd(),n.lineStart(),n.point(a,r),n.point(u,r),t=0):i!==a&&l>=Fo&&(xo(e-i)Uo?Math.atan((Math.sin(t)*(u=Math.cos(r))*Math.sin(e)-Math.sin(r)*(i=Math.cos(t))*Math.sin(n))/(i*u*o)):(t+r)/2}function Ht(n,t,e,r){var i;if(null==n)i=e*Io,r.point(-Fo,i),r.point(0,i),r.point(Fo,i),r.point(Fo,0),r.point(Fo,-i),r.point(0,-i),r.point(-Fo,-i),r.point(-Fo,0),r.point(-Fo,i);else if(xo(n[0]-t[0])>Uo){var u=n[0]a;++a){var c=t[a],f=c.length;if(f)for(var s=c[0],h=s[0],p=s[1]/2+Fo/4,g=Math.sin(p),v=Math.cos(p),d=1;;){d===f&&(d=0),n=c[d];var y=n[0],m=n[1]/2+Fo/4,M=Math.sin(m),x=Math.cos(m),b=y-h,_=b>=0?1:-1,w=_*b,S=w>Fo,k=g*M;if(ka.add(Math.atan2(k*_*Math.sin(w),v*x+k*Math.cos(w))),u+=S?b+_*Ho:b,S^h>=e^y>=e){var N=mt(dt(s),dt(n));bt(N);var E=mt(i,N);bt(E);var A=(S^b>=0?-1:1)*tn(E[2]);(r>A||r===A&&(N[0]||N[1]))&&(o+=S^b>=0?1:-1)}if(!d++)break;h=y,g=M,v=x,s=n}}return(-Uo>u||Uo>u&&-Uo>ka)^1&o}function It(n){function t(n,t){return Math.cos(n)*Math.cos(t)>u}function e(n){var e,u,l,c,f;return{lineStart:function(){c=l=!1,f=1},point:function(s,h){var p,g=[s,h],v=t(s,h),d=o?v?0:i(s,h):v?i(s+(0>s?Fo:-Fo),h):0;if(!e&&(c=l=v)&&n.lineStart(),v!==l&&(p=r(e,g),(wt(e,p)||wt(g,p))&&(g[0]+=Uo,g[1]+=Uo,v=t(g[0],g[1]))),v!==l)f=0,v?(n.lineStart(),p=r(g,e),n.point(p[0],p[1])):(p=r(e,g),n.point(p[0],p[1]),n.lineEnd()),e=p;else if(a&&e&&o^v){var y;d&u||!(y=r(g,e,!0))||(f=0,o?(n.lineStart(),n.point(y[0][0],y[0][1]),n.point(y[1][0],y[1][1]),n.lineEnd()):(n.point(y[1][0],y[1][1]),n.lineEnd(),n.lineStart(),n.point(y[0][0],y[0][1])))}!v||e&&wt(e,g)||n.point(g[0],g[1]),e=g,l=v,u=d},lineEnd:function(){l&&n.lineEnd(),e=null},clean:function(){return f|(c&&l)<<1}}}function r(n,t,e){var r=dt(n),i=dt(t),o=[1,0,0],a=mt(r,i),l=yt(a,a),c=a[0],f=l-c*c;if(!f)return!e&&n;var s=u*l/f,h=-u*c/f,p=mt(o,a),g=xt(o,s),v=xt(a,h);Mt(g,v);var d=p,y=yt(g,d),m=yt(d,d),M=y*y-m*(yt(g,g)-1);if(!(0>M)){var x=Math.sqrt(M),b=xt(d,(-y-x)/m);if(Mt(b,g),b=_t(b),!e)return b;var _,w=n[0],S=t[0],k=n[1],N=t[1];w>S&&(_=w,w=S,S=_);var E=S-w,A=xo(E-Fo)E;if(!A&&k>N&&(_=k,k=N,N=_),C?A?k+N>0^b[1]<(xo(b[0]-w)Fo^(w<=b[0]&&b[0]<=S)){var z=xt(d,(-y+x)/m);return Mt(z,g),[b,_t(z)]}}}function i(t,e){var r=o?n:Fo-n,i=0;return-r>t?i|=1:t>r&&(i|=2),-r>e?i|=4:e>r&&(i|=8),i}var u=Math.cos(n),o=u>0,a=xo(u)>Uo,l=ve(n,6*Yo);return Rt(t,e,l,o?[0,-n]:[-Fo,n-Fo])}function Yt(n,t,e,r){return function(i){var u,o=i.a,a=i.b,l=o.x,c=o.y,f=a.x,s=a.y,h=0,p=1,g=f-l,v=s-c;if(u=n-l,g||!(u>0)){if(u/=g,0>g){if(h>u)return;p>u&&(p=u)}else if(g>0){if(u>p)return;u>h&&(h=u)}if(u=e-l,g||!(0>u)){if(u/=g,0>g){if(u>p)return;u>h&&(h=u)}else if(g>0){if(h>u)return;p>u&&(p=u)}if(u=t-c,v||!(u>0)){if(u/=v,0>v){if(h>u)return;p>u&&(p=u)}else if(v>0){if(u>p)return;u>h&&(h=u)}if(u=r-c,v||!(0>u)){if(u/=v,0>v){if(u>p)return;u>h&&(h=u)}else if(v>0){if(h>u)return;p>u&&(p=u)}return h>0&&(i.a={x:l+h*g,y:c+h*v}),1>p&&(i.b={x:l+p*g,y:c+p*v}),i}}}}}}function Zt(n,t,e,r){function i(r,i){return xo(r[0]-n)0?0:3:xo(r[0]-e)0?2:1:xo(r[1]-t)0?1:0:i>0?3:2}function u(n,t){return o(n.x,t.x)}function o(n,t){var e=i(n,1),r=i(t,1);return e!==r?e-r:0===e?t[1]-n[1]:1===e?n[0]-t[0]:2===e?n[1]-t[1]:t[0]-n[0]}return function(a){function l(n){for(var t=0,e=d.length,r=n[1],i=0;e>i;++i)for(var u,o=1,a=d[i],l=a.length,c=a[0];l>o;++o)u=a[o],c[1]<=r?u[1]>r&&Q(c,u,n)>0&&++t:u[1]<=r&&Q(c,u,n)<0&&--t,c=u;return 0!==t}function c(u,a,l,c){var f=0,s=0;if(null==u||(f=i(u,l))!==(s=i(a,l))||o(u,a)<0^l>0){do c.point(0===f||3===f?n:e,f>1?r:t);while((f=(f+l+4)%4)!==s)}else c.point(a[0],a[1])}function f(i,u){return i>=n&&e>=i&&u>=t&&r>=u}function s(n,t){f(n,t)&&a.point(n,t)}function h(){C.point=g,d&&d.push(y=[]),S=!0,w=!1,b=_=NaN}function p(){v&&(g(m,M),x&&w&&E.rejoin(),v.push(E.buffer())),C.point=s,w&&a.lineEnd()}function g(n,t){n=Math.max(-Ha,Math.min(Ha,n)),t=Math.max(-Ha,Math.min(Ha,t));var e=f(n,t);if(d&&y.push([n,t]),S)m=n,M=t,x=e,S=!1,e&&(a.lineStart(),a.point(n,t));else if(e&&w)a.point(n,t);else{var r={a:{x:b,y:_},b:{x:n,y:t}};A(r)?(w||(a.lineStart(),a.point(r.a.x,r.a.y)),a.point(r.b.x,r.b.y),e||a.lineEnd(),k=!1):e&&(a.lineStart(),a.point(n,t),k=!1)}b=n,_=t,w=e}var v,d,y,m,M,x,b,_,w,S,k,N=a,E=Pt(),A=Yt(n,t,e,r),C={point:s,lineStart:h,lineEnd:p,polygonStart:function(){a=E,v=[],d=[],k=!0},polygonEnd:function(){a=N,v=ao.merge(v);var t=l([n,r]),e=k&&t,i=v.length;(e||i)&&(a.polygonStart(),e&&(a.lineStart(),c(null,null,1,a),a.lineEnd()),i&&Lt(v,u,t,c,a),a.polygonEnd()),v=d=y=null}};return C}}function Vt(n){var t=0,e=Fo/3,r=ae(n),i=r(t,e);return i.parallels=function(n){return arguments.length?r(t=n[0]*Fo/180,e=n[1]*Fo/180):[t/Fo*180,e/Fo*180]},i}function Xt(n,t){function e(n,t){var e=Math.sqrt(u-2*i*Math.sin(t))/i;return[e*Math.sin(n*=i),o-e*Math.cos(n)]}var r=Math.sin(n),i=(r+Math.sin(t))/2,u=1+r*(2*i-r),o=Math.sqrt(u)/i;return e.invert=function(n,t){var e=o-t;return[Math.atan2(n,e)/i,tn((u-(n*n+e*e)*i*i)/(2*i))]},e}function $t(){function n(n,t){Ia+=i*n-r*t,r=n,i=t}var t,e,r,i;$a.point=function(u,o){$a.point=n,t=r=u,e=i=o},$a.lineEnd=function(){n(t,e)}}function Bt(n,t){Ya>n&&(Ya=n),n>Va&&(Va=n),Za>t&&(Za=t),t>Xa&&(Xa=t)}function Wt(){function n(n,t){o.push("M",n,",",t,u)}function t(n,t){o.push("M",n,",",t),a.point=e}function e(n,t){o.push("L",n,",",t)}function r(){a.point=n}function i(){o.push("Z")}var u=Jt(4.5),o=[],a={point:n,lineStart:function(){a.point=t},lineEnd:r,polygonStart:function(){a.lineEnd=i},polygonEnd:function(){a.lineEnd=r,a.point=n},pointRadius:function(n){return u=Jt(n),a},result:function(){if(o.length){var n=o.join("");return o=[],n}}};return a}function Jt(n){return"m0,"+n+"a"+n+","+n+" 0 1,1 0,"+-2*n+"a"+n+","+n+" 0 1,1 0,"+2*n+"z"}function Gt(n,t){Ca+=n,za+=t,++La}function Kt(){function n(n,r){var i=n-t,u=r-e,o=Math.sqrt(i*i+u*u);qa+=o*(t+n)/2,Ta+=o*(e+r)/2,Ra+=o,Gt(t=n,e=r)}var t,e;Wa.point=function(r,i){Wa.point=n,Gt(t=r,e=i)}}function Qt(){Wa.point=Gt}function ne(){function n(n,t){var e=n-r,u=t-i,o=Math.sqrt(e*e+u*u);qa+=o*(r+n)/2,Ta+=o*(i+t)/2,Ra+=o,o=i*n-r*t,Da+=o*(r+n),Pa+=o*(i+t),Ua+=3*o,Gt(r=n,i=t)}var t,e,r,i;Wa.point=function(u,o){Wa.point=n,Gt(t=r=u,e=i=o)},Wa.lineEnd=function(){n(t,e)}}function te(n){function t(t,e){n.moveTo(t+o,e),n.arc(t,e,o,0,Ho)}function e(t,e){n.moveTo(t,e),a.point=r}function r(t,e){n.lineTo(t,e)}function i(){a.point=t}function u(){n.closePath()}var o=4.5,a={point:t,lineStart:function(){a.point=e},lineEnd:i,polygonStart:function(){a.lineEnd=u},polygonEnd:function(){a.lineEnd=i,a.point=t},pointRadius:function(n){return o=n,a},result:b};return a}function ee(n){function t(n){return(a?r:e)(n)}function e(t){return ue(t,function(e,r){e=n(e,r),t.point(e[0],e[1])})}function r(t){function e(e,r){e=n(e,r),t.point(e[0],e[1])}function r(){M=NaN,S.point=u,t.lineStart()}function u(e,r){var u=dt([e,r]),o=n(e,r);i(M,x,m,b,_,w,M=o[0],x=o[1],m=e,b=u[0],_=u[1],w=u[2],a,t),t.point(M,x)}function o(){S.point=e,t.lineEnd()}function l(){ +r(),S.point=c,S.lineEnd=f}function c(n,t){u(s=n,h=t),p=M,g=x,v=b,d=_,y=w,S.point=u}function f(){i(M,x,m,b,_,w,p,g,s,v,d,y,a,t),S.lineEnd=o,o()}var s,h,p,g,v,d,y,m,M,x,b,_,w,S={point:e,lineStart:r,lineEnd:o,polygonStart:function(){t.polygonStart(),S.lineStart=l},polygonEnd:function(){t.polygonEnd(),S.lineStart=r}};return S}function i(t,e,r,a,l,c,f,s,h,p,g,v,d,y){var m=f-t,M=s-e,x=m*m+M*M;if(x>4*u&&d--){var b=a+p,_=l+g,w=c+v,S=Math.sqrt(b*b+_*_+w*w),k=Math.asin(w/=S),N=xo(xo(w)-1)u||xo((m*z+M*L)/x-.5)>.3||o>a*p+l*g+c*v)&&(i(t,e,r,a,l,c,A,C,N,b/=S,_/=S,w,d,y),y.point(A,C),i(A,C,N,b,_,w,f,s,h,p,g,v,d,y))}}var u=.5,o=Math.cos(30*Yo),a=16;return t.precision=function(n){return arguments.length?(a=(u=n*n)>0&&16,t):Math.sqrt(u)},t}function re(n){var t=ee(function(t,e){return n([t*Zo,e*Zo])});return function(n){return le(t(n))}}function ie(n){this.stream=n}function ue(n,t){return{point:t,sphere:function(){n.sphere()},lineStart:function(){n.lineStart()},lineEnd:function(){n.lineEnd()},polygonStart:function(){n.polygonStart()},polygonEnd:function(){n.polygonEnd()}}}function oe(n){return ae(function(){return n})()}function ae(n){function t(n){return n=a(n[0]*Yo,n[1]*Yo),[n[0]*h+l,c-n[1]*h]}function e(n){return n=a.invert((n[0]-l)/h,(c-n[1])/h),n&&[n[0]*Zo,n[1]*Zo]}function r(){a=Ct(o=se(y,M,x),u);var n=u(v,d);return l=p-n[0]*h,c=g+n[1]*h,i()}function i(){return f&&(f.valid=!1,f=null),t}var u,o,a,l,c,f,s=ee(function(n,t){return n=u(n,t),[n[0]*h+l,c-n[1]*h]}),h=150,p=480,g=250,v=0,d=0,y=0,M=0,x=0,b=Fa,_=m,w=null,S=null;return t.stream=function(n){return f&&(f.valid=!1),f=le(b(o,s(_(n)))),f.valid=!0,f},t.clipAngle=function(n){return arguments.length?(b=null==n?(w=n,Fa):It((w=+n)*Yo),i()):w},t.clipExtent=function(n){return arguments.length?(S=n,_=n?Zt(n[0][0],n[0][1],n[1][0],n[1][1]):m,i()):S},t.scale=function(n){return arguments.length?(h=+n,r()):h},t.translate=function(n){return arguments.length?(p=+n[0],g=+n[1],r()):[p,g]},t.center=function(n){return arguments.length?(v=n[0]%360*Yo,d=n[1]%360*Yo,r()):[v*Zo,d*Zo]},t.rotate=function(n){return arguments.length?(y=n[0]%360*Yo,M=n[1]%360*Yo,x=n.length>2?n[2]%360*Yo:0,r()):[y*Zo,M*Zo,x*Zo]},ao.rebind(t,s,"precision"),function(){return u=n.apply(this,arguments),t.invert=u.invert&&e,r()}}function le(n){return ue(n,function(t,e){n.point(t*Yo,e*Yo)})}function ce(n,t){return[n,t]}function fe(n,t){return[n>Fo?n-Ho:-Fo>n?n+Ho:n,t]}function se(n,t,e){return n?t||e?Ct(pe(n),ge(t,e)):pe(n):t||e?ge(t,e):fe}function he(n){return function(t,e){return t+=n,[t>Fo?t-Ho:-Fo>t?t+Ho:t,e]}}function pe(n){var t=he(n);return t.invert=he(-n),t}function ge(n,t){function e(n,t){var e=Math.cos(t),a=Math.cos(n)*e,l=Math.sin(n)*e,c=Math.sin(t),f=c*r+a*i;return[Math.atan2(l*u-f*o,a*r-c*i),tn(f*u+l*o)]}var r=Math.cos(n),i=Math.sin(n),u=Math.cos(t),o=Math.sin(t);return e.invert=function(n,t){var e=Math.cos(t),a=Math.cos(n)*e,l=Math.sin(n)*e,c=Math.sin(t),f=c*u-l*o;return[Math.atan2(l*u+c*o,a*r+f*i),tn(f*r-a*i)]},e}function ve(n,t){var e=Math.cos(n),r=Math.sin(n);return function(i,u,o,a){var l=o*t;null!=i?(i=de(e,i),u=de(e,u),(o>0?u>i:i>u)&&(i+=o*Ho)):(i=n+o*Ho,u=n-.5*l);for(var c,f=i;o>0?f>u:u>f;f-=l)a.point((c=_t([e,-r*Math.cos(f),-r*Math.sin(f)]))[0],c[1])}}function de(n,t){var e=dt(t);e[0]-=n,bt(e);var r=nn(-e[1]);return((-e[2]<0?-r:r)+2*Math.PI-Uo)%(2*Math.PI)}function ye(n,t,e){var r=ao.range(n,t-Uo,e).concat(t);return function(n){return r.map(function(t){return[n,t]})}}function me(n,t,e){var r=ao.range(n,t-Uo,e).concat(t);return function(n){return r.map(function(t){return[t,n]})}}function Me(n){return n.source}function xe(n){return n.target}function be(n,t,e,r){var i=Math.cos(t),u=Math.sin(t),o=Math.cos(r),a=Math.sin(r),l=i*Math.cos(n),c=i*Math.sin(n),f=o*Math.cos(e),s=o*Math.sin(e),h=2*Math.asin(Math.sqrt(on(r-t)+i*o*on(e-n))),p=1/Math.sin(h),g=h?function(n){var t=Math.sin(n*=h)*p,e=Math.sin(h-n)*p,r=e*l+t*f,i=e*c+t*s,o=e*u+t*a;return[Math.atan2(i,r)*Zo,Math.atan2(o,Math.sqrt(r*r+i*i))*Zo]}:function(){return[n*Zo,t*Zo]};return g.distance=h,g}function _e(){function n(n,i){var u=Math.sin(i*=Yo),o=Math.cos(i),a=xo((n*=Yo)-t),l=Math.cos(a);Ja+=Math.atan2(Math.sqrt((a=o*Math.sin(a))*a+(a=r*u-e*o*l)*a),e*u+r*o*l),t=n,e=u,r=o}var t,e,r;Ga.point=function(i,u){t=i*Yo,e=Math.sin(u*=Yo),r=Math.cos(u),Ga.point=n},Ga.lineEnd=function(){Ga.point=Ga.lineEnd=b}}function we(n,t){function e(t,e){var r=Math.cos(t),i=Math.cos(e),u=n(r*i);return[u*i*Math.sin(t),u*Math.sin(e)]}return e.invert=function(n,e){var r=Math.sqrt(n*n+e*e),i=t(r),u=Math.sin(i),o=Math.cos(i);return[Math.atan2(n*u,r*o),Math.asin(r&&e*u/r)]},e}function Se(n,t){function e(n,t){o>0?-Io+Uo>t&&(t=-Io+Uo):t>Io-Uo&&(t=Io-Uo);var e=o/Math.pow(i(t),u);return[e*Math.sin(u*n),o-e*Math.cos(u*n)]}var r=Math.cos(n),i=function(n){return Math.tan(Fo/4+n/2)},u=n===t?Math.sin(n):Math.log(r/Math.cos(t))/Math.log(i(t)/i(n)),o=r*Math.pow(i(n),u)/u;return u?(e.invert=function(n,t){var e=o-t,r=K(u)*Math.sqrt(n*n+e*e);return[Math.atan2(n,e)/u,2*Math.atan(Math.pow(o/r,1/u))-Io]},e):Ne}function ke(n,t){function e(n,t){var e=u-t;return[e*Math.sin(i*n),u-e*Math.cos(i*n)]}var r=Math.cos(n),i=n===t?Math.sin(n):(r-Math.cos(t))/(t-n),u=r/i+n;return xo(i)i;i++){for(;r>1&&Q(n[e[r-2]],n[e[r-1]],n[i])<=0;)--r;e[r++]=i}return e.slice(0,r)}function qe(n,t){return n[0]-t[0]||n[1]-t[1]}function Te(n,t,e){return(e[0]-t[0])*(n[1]-t[1])<(e[1]-t[1])*(n[0]-t[0])}function Re(n,t,e,r){var i=n[0],u=e[0],o=t[0]-i,a=r[0]-u,l=n[1],c=e[1],f=t[1]-l,s=r[1]-c,h=(a*(l-c)-s*(i-u))/(s*o-a*f);return[i+h*o,l+h*f]}function De(n){var t=n[0],e=n[n.length-1];return!(t[0]-e[0]||t[1]-e[1])}function Pe(){rr(this),this.edge=this.site=this.circle=null}function Ue(n){var t=cl.pop()||new Pe;return t.site=n,t}function je(n){Be(n),ol.remove(n),cl.push(n),rr(n)}function Fe(n){var t=n.circle,e=t.x,r=t.cy,i={x:e,y:r},u=n.P,o=n.N,a=[n];je(n);for(var l=u;l.circle&&xo(e-l.circle.x)f;++f)c=a[f],l=a[f-1],nr(c.edge,l.site,c.site,i);l=a[0],c=a[s-1],c.edge=Ke(l.site,c.site,null,i),$e(l),$e(c)}function He(n){for(var t,e,r,i,u=n.x,o=n.y,a=ol._;a;)if(r=Oe(a,o)-u,r>Uo)a=a.L;else{if(i=u-Ie(a,o),!(i>Uo)){r>-Uo?(t=a.P,e=a):i>-Uo?(t=a,e=a.N):t=e=a;break}if(!a.R){t=a;break}a=a.R}var l=Ue(n);if(ol.insert(t,l),t||e){if(t===e)return Be(t),e=Ue(t.site),ol.insert(l,e),l.edge=e.edge=Ke(t.site,l.site),$e(t),void $e(e);if(!e)return void(l.edge=Ke(t.site,l.site));Be(t),Be(e);var c=t.site,f=c.x,s=c.y,h=n.x-f,p=n.y-s,g=e.site,v=g.x-f,d=g.y-s,y=2*(h*d-p*v),m=h*h+p*p,M=v*v+d*d,x={x:(d*m-p*M)/y+f,y:(h*M-v*m)/y+s};nr(e.edge,c,g,x),l.edge=Ke(c,n,null,x),e.edge=Ke(n,g,null,x),$e(t),$e(e)}}function Oe(n,t){var e=n.site,r=e.x,i=e.y,u=i-t;if(!u)return r;var o=n.P;if(!o)return-(1/0);e=o.site;var a=e.x,l=e.y,c=l-t;if(!c)return a;var f=a-r,s=1/u-1/c,h=f/c;return s?(-h+Math.sqrt(h*h-2*s*(f*f/(-2*c)-l+c/2+i-u/2)))/s+r:(r+a)/2}function Ie(n,t){var e=n.N;if(e)return Oe(e,t);var r=n.site;return r.y===t?r.x:1/0}function Ye(n){this.site=n,this.edges=[]}function Ze(n){for(var t,e,r,i,u,o,a,l,c,f,s=n[0][0],h=n[1][0],p=n[0][1],g=n[1][1],v=ul,d=v.length;d--;)if(u=v[d],u&&u.prepare())for(a=u.edges,l=a.length,o=0;l>o;)f=a[o].end(),r=f.x,i=f.y,c=a[++o%l].start(),t=c.x,e=c.y,(xo(r-t)>Uo||xo(i-e)>Uo)&&(a.splice(o,0,new tr(Qe(u.site,f,xo(r-s)Uo?{x:s,y:xo(t-s)Uo?{x:xo(e-g)Uo?{x:h,y:xo(t-h)Uo?{x:xo(e-p)=-jo)){var p=l*l+c*c,g=f*f+s*s,v=(s*p-c*g)/h,d=(l*g-f*p)/h,s=d+a,y=fl.pop()||new Xe;y.arc=n,y.site=i,y.x=v+o,y.y=s+Math.sqrt(v*v+d*d),y.cy=s,n.circle=y;for(var m=null,M=ll._;M;)if(y.yd||d>=a)return;if(h>g){if(u){if(u.y>=c)return}else u={x:d,y:l};e={x:d,y:c}}else{if(u){if(u.yr||r>1)if(h>g){if(u){if(u.y>=c)return}else u={x:(l-i)/r,y:l};e={x:(c-i)/r,y:c}}else{if(u){if(u.yp){if(u){if(u.x>=a)return}else u={x:o,y:r*o+i};e={x:a,y:r*a+i}}else{if(u){if(u.xu||s>o||r>h||i>p)){if(g=n.point){var g,v=t-n.x,d=e-n.y,y=v*v+d*d;if(l>y){var m=Math.sqrt(l=y);r=t-m,i=e-m,u=t+m,o=e+m,a=g}}for(var M=n.nodes,x=.5*(f+h),b=.5*(s+p),_=t>=x,w=e>=b,S=w<<1|_,k=S+4;k>S;++S)if(n=M[3&S])switch(3&S){case 0:c(n,f,s,x,b);break;case 1:c(n,x,s,h,b);break;case 2:c(n,f,b,x,p);break;case 3:c(n,x,b,h,p)}}}(n,r,i,u,o),a}function vr(n,t){n=ao.rgb(n),t=ao.rgb(t);var e=n.r,r=n.g,i=n.b,u=t.r-e,o=t.g-r,a=t.b-i;return function(n){return"#"+bn(Math.round(e+u*n))+bn(Math.round(r+o*n))+bn(Math.round(i+a*n))}}function dr(n,t){var e,r={},i={};for(e in n)e in t?r[e]=Mr(n[e],t[e]):i[e]=n[e];for(e in t)e in n||(i[e]=t[e]);return function(n){for(e in r)i[e]=r[e](n);return i}}function yr(n,t){return n=+n,t=+t,function(e){return n*(1-e)+t*e}}function mr(n,t){var e,r,i,u=hl.lastIndex=pl.lastIndex=0,o=-1,a=[],l=[];for(n+="",t+="";(e=hl.exec(n))&&(r=pl.exec(t));)(i=r.index)>u&&(i=t.slice(u,i),a[o]?a[o]+=i:a[++o]=i),(e=e[0])===(r=r[0])?a[o]?a[o]+=r:a[++o]=r:(a[++o]=null,l.push({i:o,x:yr(e,r)})),u=pl.lastIndex;return ur;++r)a[(e=l[r]).i]=e.x(n);return a.join("")})}function Mr(n,t){for(var e,r=ao.interpolators.length;--r>=0&&!(e=ao.interpolators[r](n,t)););return e}function xr(n,t){var e,r=[],i=[],u=n.length,o=t.length,a=Math.min(n.length,t.length);for(e=0;a>e;++e)r.push(Mr(n[e],t[e]));for(;u>e;++e)i[e]=n[e];for(;o>e;++e)i[e]=t[e];return function(n){for(e=0;a>e;++e)i[e]=r[e](n);return i}}function br(n){return function(t){return 0>=t?0:t>=1?1:n(t)}}function _r(n){return function(t){return 1-n(1-t)}}function wr(n){return function(t){return.5*(.5>t?n(2*t):2-n(2-2*t))}}function Sr(n){return n*n}function kr(n){return n*n*n}function Nr(n){if(0>=n)return 0;if(n>=1)return 1;var t=n*n,e=t*n;return 4*(.5>n?e:3*(n-t)+e-.75)}function Er(n){return function(t){return Math.pow(t,n)}}function Ar(n){return 1-Math.cos(n*Io)}function Cr(n){return Math.pow(2,10*(n-1))}function zr(n){return 1-Math.sqrt(1-n*n)}function Lr(n,t){var e;return arguments.length<2&&(t=.45),arguments.length?e=t/Ho*Math.asin(1/n):(n=1,e=t/4),function(r){return 1+n*Math.pow(2,-10*r)*Math.sin((r-e)*Ho/t)}}function qr(n){return n||(n=1.70158),function(t){return t*t*((n+1)*t-n)}}function Tr(n){return 1/2.75>n?7.5625*n*n:2/2.75>n?7.5625*(n-=1.5/2.75)*n+.75:2.5/2.75>n?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375}function Rr(n,t){n=ao.hcl(n),t=ao.hcl(t);var e=n.h,r=n.c,i=n.l,u=t.h-e,o=t.c-r,a=t.l-i;return isNaN(o)&&(o=0,r=isNaN(r)?t.c:r),isNaN(u)?(u=0,e=isNaN(e)?t.h:e):u>180?u-=360:-180>u&&(u+=360),function(n){return sn(e+u*n,r+o*n,i+a*n)+""}}function Dr(n,t){n=ao.hsl(n),t=ao.hsl(t);var e=n.h,r=n.s,i=n.l,u=t.h-e,o=t.s-r,a=t.l-i;return isNaN(o)&&(o=0,r=isNaN(r)?t.s:r),isNaN(u)?(u=0,e=isNaN(e)?t.h:e):u>180?u-=360:-180>u&&(u+=360),function(n){return cn(e+u*n,r+o*n,i+a*n)+""}}function Pr(n,t){n=ao.lab(n),t=ao.lab(t);var e=n.l,r=n.a,i=n.b,u=t.l-e,o=t.a-r,a=t.b-i;return function(n){return pn(e+u*n,r+o*n,i+a*n)+""}}function Ur(n,t){return t-=n,function(e){return Math.round(n+t*e)}}function jr(n){var t=[n.a,n.b],e=[n.c,n.d],r=Hr(t),i=Fr(t,e),u=Hr(Or(e,t,-i))||0;t[0]*e[1]180?t+=360:t-n>180&&(n+=360),r.push({i:e.push(Ir(e)+"rotate(",null,")")-2,x:yr(n,t)})):t&&e.push(Ir(e)+"rotate("+t+")")}function Vr(n,t,e,r){n!==t?r.push({i:e.push(Ir(e)+"skewX(",null,")")-2,x:yr(n,t)}):t&&e.push(Ir(e)+"skewX("+t+")")}function Xr(n,t,e,r){if(n[0]!==t[0]||n[1]!==t[1]){var i=e.push(Ir(e)+"scale(",null,",",null,")");r.push({i:i-4,x:yr(n[0],t[0])},{i:i-2,x:yr(n[1],t[1])})}else 1===t[0]&&1===t[1]||e.push(Ir(e)+"scale("+t+")")}function $r(n,t){var e=[],r=[];return n=ao.transform(n),t=ao.transform(t),Yr(n.translate,t.translate,e,r),Zr(n.rotate,t.rotate,e,r),Vr(n.skew,t.skew,e,r),Xr(n.scale,t.scale,e,r),n=t=null,function(n){for(var t,i=-1,u=r.length;++i=0;)e.push(i[r])}function oi(n,t){for(var e=[n],r=[];null!=(n=e.pop());)if(r.push(n),(u=n.children)&&(i=u.length))for(var i,u,o=-1;++oe;++e)(t=n[e][1])>i&&(r=e,i=t);return r}function yi(n){return n.reduce(mi,0)}function mi(n,t){return n+t[1]}function Mi(n,t){return xi(n,Math.ceil(Math.log(t.length)/Math.LN2+1))}function xi(n,t){for(var e=-1,r=+n[0],i=(n[1]-r)/t,u=[];++e<=t;)u[e]=i*e+r;return u}function bi(n){return[ao.min(n),ao.max(n)]}function _i(n,t){return n.value-t.value}function wi(n,t){var e=n._pack_next;n._pack_next=t,t._pack_prev=n,t._pack_next=e,e._pack_prev=t}function Si(n,t){n._pack_next=t,t._pack_prev=n}function ki(n,t){var e=t.x-n.x,r=t.y-n.y,i=n.r+t.r;return.999*i*i>e*e+r*r}function Ni(n){function t(n){f=Math.min(n.x-n.r,f),s=Math.max(n.x+n.r,s),h=Math.min(n.y-n.r,h),p=Math.max(n.y+n.r,p)}if((e=n.children)&&(c=e.length)){var e,r,i,u,o,a,l,c,f=1/0,s=-(1/0),h=1/0,p=-(1/0);if(e.forEach(Ei),r=e[0],r.x=-r.r,r.y=0,t(r),c>1&&(i=e[1],i.x=i.r,i.y=0,t(i),c>2))for(u=e[2],zi(r,i,u),t(u),wi(r,u),r._pack_prev=u,wi(u,i),i=r._pack_next,o=3;c>o;o++){zi(r,i,u=e[o]);var g=0,v=1,d=1;for(a=i._pack_next;a!==i;a=a._pack_next,v++)if(ki(a,u)){g=1;break}if(1==g)for(l=r._pack_prev;l!==a._pack_prev&&!ki(l,u);l=l._pack_prev,d++);g?(d>v||v==d&&i.ro;o++)u=e[o],u.x-=y,u.y-=m,M=Math.max(M,u.r+Math.sqrt(u.x*u.x+u.y*u.y));n.r=M,e.forEach(Ai)}}function Ei(n){n._pack_next=n._pack_prev=n}function Ai(n){delete n._pack_next,delete n._pack_prev}function Ci(n,t,e,r){var i=n.children;if(n.x=t+=r*n.x,n.y=e+=r*n.y,n.r*=r,i)for(var u=-1,o=i.length;++u=0;)t=i[u],t.z+=e,t.m+=e,e+=t.s+(r+=t.c)}function Pi(n,t,e){return n.a.parent===t.parent?n.a:e}function Ui(n){return 1+ao.max(n,function(n){return n.y})}function ji(n){return n.reduce(function(n,t){return n+t.x},0)/n.length}function Fi(n){var t=n.children;return t&&t.length?Fi(t[0]):n}function Hi(n){var t,e=n.children;return e&&(t=e.length)?Hi(e[t-1]):n}function Oi(n){return{x:n.x,y:n.y,dx:n.dx,dy:n.dy}}function Ii(n,t){var e=n.x+t[3],r=n.y+t[0],i=n.dx-t[1]-t[3],u=n.dy-t[0]-t[2];return 0>i&&(e+=i/2,i=0),0>u&&(r+=u/2,u=0),{x:e,y:r,dx:i,dy:u}}function Yi(n){var t=n[0],e=n[n.length-1];return e>t?[t,e]:[e,t]}function Zi(n){return n.rangeExtent?n.rangeExtent():Yi(n.range())}function Vi(n,t,e,r){var i=e(n[0],n[1]),u=r(t[0],t[1]);return function(n){return u(i(n))}}function Xi(n,t){var e,r=0,i=n.length-1,u=n[r],o=n[i];return u>o&&(e=r,r=i,i=e,e=u,u=o,o=e),n[r]=t.floor(u),n[i]=t.ceil(o),n}function $i(n){return n?{floor:function(t){return Math.floor(t/n)*n},ceil:function(t){return Math.ceil(t/n)*n}}:Sl}function Bi(n,t,e,r){var i=[],u=[],o=0,a=Math.min(n.length,t.length)-1;for(n[a]2?Bi:Vi,l=r?Wr:Br;return o=i(n,t,l,e),a=i(t,n,l,Mr),u}function u(n){return o(n)}var o,a;return u.invert=function(n){return a(n)},u.domain=function(t){return arguments.length?(n=t.map(Number),i()):n},u.range=function(n){return arguments.length?(t=n,i()):t},u.rangeRound=function(n){return u.range(n).interpolate(Ur)},u.clamp=function(n){return arguments.length?(r=n,i()):r},u.interpolate=function(n){return arguments.length?(e=n,i()):e},u.ticks=function(t){return Qi(n,t)},u.tickFormat=function(t,e){return nu(n,t,e)},u.nice=function(t){return Gi(n,t),i()},u.copy=function(){return Wi(n,t,e,r)},i()}function Ji(n,t){return ao.rebind(n,t,"range","rangeRound","interpolate","clamp")}function Gi(n,t){return Xi(n,$i(Ki(n,t)[2])),Xi(n,$i(Ki(n,t)[2])),n}function Ki(n,t){null==t&&(t=10);var e=Yi(n),r=e[1]-e[0],i=Math.pow(10,Math.floor(Math.log(r/t)/Math.LN10)),u=t/r*i;return.15>=u?i*=10:.35>=u?i*=5:.75>=u&&(i*=2),e[0]=Math.ceil(e[0]/i)*i,e[1]=Math.floor(e[1]/i)*i+.5*i,e[2]=i,e}function Qi(n,t){return ao.range.apply(ao,Ki(n,t))}function nu(n,t,e){var r=Ki(n,t);if(e){var i=ha.exec(e);if(i.shift(),"s"===i[8]){var u=ao.formatPrefix(Math.max(xo(r[0]),xo(r[1])));return i[7]||(i[7]="."+tu(u.scale(r[2]))),i[8]="f",e=ao.format(i.join("")),function(n){return e(u.scale(n))+u.symbol}}i[7]||(i[7]="."+eu(i[8],r)),e=i.join("")}else e=",."+tu(r[2])+"f";return ao.format(e)}function tu(n){return-Math.floor(Math.log(n)/Math.LN10+.01)}function eu(n,t){var e=tu(t[2]);return n in kl?Math.abs(e-tu(Math.max(xo(t[0]),xo(t[1]))))+ +("e"!==n):e-2*("%"===n)}function ru(n,t,e,r){function i(n){return(e?Math.log(0>n?0:n):-Math.log(n>0?0:-n))/Math.log(t)}function u(n){return e?Math.pow(t,n):-Math.pow(t,-n)}function o(t){return n(i(t))}return o.invert=function(t){return u(n.invert(t))},o.domain=function(t){return arguments.length?(e=t[0]>=0,n.domain((r=t.map(Number)).map(i)),o):r},o.base=function(e){return arguments.length?(t=+e,n.domain(r.map(i)),o):t},o.nice=function(){var t=Xi(r.map(i),e?Math:El);return n.domain(t),r=t.map(u),o},o.ticks=function(){var n=Yi(r),o=[],a=n[0],l=n[1],c=Math.floor(i(a)),f=Math.ceil(i(l)),s=t%1?2:t;if(isFinite(f-c)){if(e){for(;f>c;c++)for(var h=1;s>h;h++)o.push(u(c)*h);o.push(u(c))}else for(o.push(u(c));c++0;h--)o.push(u(c)*h);for(c=0;o[c]l;f--);o=o.slice(c,f)}return o},o.tickFormat=function(n,e){if(!arguments.length)return Nl;arguments.length<2?e=Nl:"function"!=typeof e&&(e=ao.format(e));var r=Math.max(1,t*n/o.ticks().length);return function(n){var o=n/u(Math.round(i(n)));return t-.5>o*t&&(o*=t),r>=o?e(n):""}},o.copy=function(){return ru(n.copy(),t,e,r)},Ji(o,n)}function iu(n,t,e){function r(t){return n(i(t))}var i=uu(t),u=uu(1/t);return r.invert=function(t){return u(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain((e=t.map(Number)).map(i)),r):e},r.ticks=function(n){return Qi(e,n)},r.tickFormat=function(n,t){return nu(e,n,t)},r.nice=function(n){return r.domain(Gi(e,n))},r.exponent=function(o){return arguments.length?(i=uu(t=o),u=uu(1/t),n.domain(e.map(i)),r):t},r.copy=function(){return iu(n.copy(),t,e)},Ji(r,n)}function uu(n){return function(t){return 0>t?-Math.pow(-t,n):Math.pow(t,n)}}function ou(n,t){function e(e){return u[((i.get(e)||("range"===t.t?i.set(e,n.push(e)):NaN))-1)%u.length]}function r(t,e){return ao.range(n.length).map(function(n){return t+e*n})}var i,u,o;return e.domain=function(r){if(!arguments.length)return n;n=[],i=new c;for(var u,o=-1,a=r.length;++oe?[NaN,NaN]:[e>0?a[e-1]:n[0],et?NaN:t/u+n,[t,t+1/u]},r.copy=function(){return lu(n,t,e)},i()}function cu(n,t){function e(e){return e>=e?t[ao.bisect(n,e)]:void 0}return e.domain=function(t){return arguments.length?(n=t,e):n},e.range=function(n){return arguments.length?(t=n,e):t},e.invertExtent=function(e){return e=t.indexOf(e),[n[e-1],n[e]]},e.copy=function(){return cu(n,t)},e}function fu(n){function t(n){return+n}return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=e.map(t),t):n},t.ticks=function(t){return Qi(n,t)},t.tickFormat=function(t,e){return nu(n,t,e)},t.copy=function(){return fu(n)},t}function su(){return 0}function hu(n){return n.innerRadius}function pu(n){return n.outerRadius}function gu(n){return n.startAngle}function vu(n){return n.endAngle}function du(n){return n&&n.padAngle}function yu(n,t,e,r){return(n-e)*t-(t-r)*n>0?0:1}function mu(n,t,e,r,i){var u=n[0]-t[0],o=n[1]-t[1],a=(i?r:-r)/Math.sqrt(u*u+o*o),l=a*o,c=-a*u,f=n[0]+l,s=n[1]+c,h=t[0]+l,p=t[1]+c,g=(f+h)/2,v=(s+p)/2,d=h-f,y=p-s,m=d*d+y*y,M=e-r,x=f*p-h*s,b=(0>y?-1:1)*Math.sqrt(Math.max(0,M*M*m-x*x)),_=(x*y-d*b)/m,w=(-x*d-y*b)/m,S=(x*y+d*b)/m,k=(-x*d+y*b)/m,N=_-g,E=w-v,A=S-g,C=k-v;return N*N+E*E>A*A+C*C&&(_=S,w=k),[[_-l,w-c],[_*e/M,w*e/M]]}function Mu(n){function t(t){function o(){c.push("M",u(n(f),a))}for(var l,c=[],f=[],s=-1,h=t.length,p=En(e),g=En(r);++s1?n.join("L"):n+"Z"}function bu(n){return n.join("L")+"Z"}function _u(n){for(var t=0,e=n.length,r=n[0],i=[r[0],",",r[1]];++t1&&i.push("H",r[0]),i.join("")}function wu(n){for(var t=0,e=n.length,r=n[0],i=[r[0],",",r[1]];++t1){a=t[1],u=n[l],l++,r+="C"+(i[0]+o[0])+","+(i[1]+o[1])+","+(u[0]-a[0])+","+(u[1]-a[1])+","+u[0]+","+u[1];for(var c=2;c9&&(i=3*t/Math.sqrt(i),o[a]=i*e,o[a+1]=i*r));for(a=-1;++a<=l;)i=(n[Math.min(l,a+1)][0]-n[Math.max(0,a-1)][0])/(6*(1+o[a]*o[a])),u.push([i||0,o[a]*i||0]);return u}function Fu(n){return n.length<3?xu(n):n[0]+Au(n,ju(n))}function Hu(n){for(var t,e,r,i=-1,u=n.length;++i=t?o(n-t):void(f.c=o)}function o(e){var i=g.active,u=g[i];u&&(u.timer.c=null,u.timer.t=NaN,--g.count,delete g[i],u.event&&u.event.interrupt.call(n,n.__data__,u.index));for(var o in g)if(r>+o){var c=g[o];c.timer.c=null,c.timer.t=NaN,--g.count,delete g[o]}f.c=a,qn(function(){return f.c&&a(e||1)&&(f.c=null,f.t=NaN),1},0,l),g.active=r,v.event&&v.event.start.call(n,n.__data__,t),p=[],v.tween.forEach(function(e,r){(r=r.call(n,n.__data__,t))&&p.push(r)}),h=v.ease,s=v.duration}function a(i){for(var u=i/s,o=h(u),a=p.length;a>0;)p[--a].call(n,o);return u>=1?(v.event&&v.event.end.call(n,n.__data__,t),--g.count?delete g[r]:delete n[e],1):void 0}var l,f,s,h,p,g=n[e]||(n[e]={active:0,count:0}),v=g[r];v||(l=i.time,f=qn(u,0,l),v=g[r]={tween:new c,time:l,timer:f,delay:i.delay,duration:i.duration,ease:i.ease,index:t},i=null,++g.count)}function no(n,t,e){n.attr("transform",function(n){var r=t(n);return"translate("+(isFinite(r)?r:e(n))+",0)"})}function to(n,t,e){n.attr("transform",function(n){var r=t(n);return"translate(0,"+(isFinite(r)?r:e(n))+")"})}function eo(n){return n.toISOString()}function ro(n,t,e){function r(t){return n(t)}function i(n,e){var r=n[1]-n[0],i=r/e,u=ao.bisect(Kl,i);return u==Kl.length?[t.year,Ki(n.map(function(n){return n/31536e6}),e)[2]]:u?t[i/Kl[u-1]1?{floor:function(t){for(;e(t=n.floor(t));)t=io(t-1);return t},ceil:function(t){for(;e(t=n.ceil(t));)t=io(+t+1);return t}}:n))},r.ticks=function(n,t){var e=Yi(r.domain()),u=null==n?i(e,10):"number"==typeof n?i(e,n):!n.range&&[{range:n},t];return u&&(n=u[0],t=u[1]),n.range(e[0],io(+e[1]+1),1>t?1:t)},r.tickFormat=function(){return e},r.copy=function(){return ro(n.copy(),t,e)},Ji(r,n)}function io(n){return new Date(n)}function uo(n){return JSON.parse(n.responseText)}function oo(n){var t=fo.createRange();return t.selectNode(fo.body),t.createContextualFragment(n.responseText)}var ao={version:"3.5.17"},lo=[].slice,co=function(n){return lo.call(n)},fo=this.document;if(fo)try{co(fo.documentElement.childNodes)[0].nodeType}catch(so){co=function(n){for(var t=n.length,e=new Array(t);t--;)e[t]=n[t];return e}}if(Date.now||(Date.now=function(){return+new Date}),fo)try{fo.createElement("DIV").style.setProperty("opacity",0,"")}catch(ho){var po=this.Element.prototype,go=po.setAttribute,vo=po.setAttributeNS,yo=this.CSSStyleDeclaration.prototype,mo=yo.setProperty;po.setAttribute=function(n,t){go.call(this,n,t+"")},po.setAttributeNS=function(n,t,e){vo.call(this,n,t,e+"")},yo.setProperty=function(n,t,e){mo.call(this,n,t+"",e)}}ao.ascending=e,ao.descending=function(n,t){return n>t?-1:t>n?1:t>=n?0:NaN},ao.min=function(n,t){var e,r,i=-1,u=n.length;if(1===arguments.length){for(;++i=r){e=r;break}for(;++ir&&(e=r)}else{for(;++i=r){e=r;break}for(;++ir&&(e=r)}return e},ao.max=function(n,t){var e,r,i=-1,u=n.length;if(1===arguments.length){for(;++i=r){e=r;break}for(;++ie&&(e=r)}else{for(;++i=r){e=r;break}for(;++ie&&(e=r)}return e},ao.extent=function(n,t){var e,r,i,u=-1,o=n.length;if(1===arguments.length){for(;++u=r){e=i=r;break}for(;++ur&&(e=r),r>i&&(i=r))}else{for(;++u=r){e=i=r;break}for(;++ur&&(e=r),r>i&&(i=r))}return[e,i]},ao.sum=function(n,t){var e,r=0,u=n.length,o=-1;if(1===arguments.length)for(;++o1?l/(f-1):void 0},ao.deviation=function(){var n=ao.variance.apply(this,arguments);return n?Math.sqrt(n):n};var Mo=u(e);ao.bisectLeft=Mo.left,ao.bisect=ao.bisectRight=Mo.right,ao.bisector=function(n){return u(1===n.length?function(t,r){return e(n(t),r)}:n)},ao.shuffle=function(n,t,e){(u=arguments.length)<3&&(e=n.length,2>u&&(t=0));for(var r,i,u=e-t;u;)i=Math.random()*u--|0,r=n[u+t],n[u+t]=n[i+t],n[i+t]=r;return n},ao.permute=function(n,t){for(var e=t.length,r=new Array(e);e--;)r[e]=n[t[e]];return r},ao.pairs=function(n){for(var t,e=0,r=n.length-1,i=n[0],u=new Array(0>r?0:r);r>e;)u[e]=[t=i,i=n[++e]];return u},ao.transpose=function(n){if(!(i=n.length))return[];for(var t=-1,e=ao.min(n,o),r=new Array(e);++t=0;)for(r=n[i],t=r.length;--t>=0;)e[--o]=r[t];return e};var xo=Math.abs;ao.range=function(n,t,e){if(arguments.length<3&&(e=1,arguments.length<2&&(t=n,n=0)),(t-n)/e===1/0)throw new Error("infinite range");var r,i=[],u=a(xo(e)),o=-1;if(n*=u,t*=u,e*=u,0>e)for(;(r=n+e*++o)>t;)i.push(r/u);else for(;(r=n+e*++o)=u.length)return r?r.call(i,o):e?o.sort(e):o;for(var l,f,s,h,p=-1,g=o.length,v=u[a++],d=new c;++p=u.length)return n;var r=[],i=o[e++];return n.forEach(function(n,i){r.push({key:n,values:t(i,e)})}),i?r.sort(function(n,t){return i(n.key,t.key)}):r}var e,r,i={},u=[],o=[];return i.map=function(t,e){return n(e,t,0)},i.entries=function(e){return t(n(ao.map,e,0),0)},i.key=function(n){return u.push(n),i},i.sortKeys=function(n){return o[u.length-1]=n,i},i.sortValues=function(n){return e=n,i},i.rollup=function(n){return r=n,i},i},ao.set=function(n){var t=new y;if(n)for(var e=0,r=n.length;r>e;++e)t.add(n[e]);return t},l(y,{has:h,add:function(n){return this._[f(n+="")]=!0,n},remove:p,values:g,size:v,empty:d,forEach:function(n){for(var t in this._)n.call(this,s(t))}}),ao.behavior={},ao.rebind=function(n,t){for(var e,r=1,i=arguments.length;++r=0&&(r=n.slice(e+1),n=n.slice(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(2===arguments.length){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},ao.event=null,ao.requote=function(n){return n.replace(So,"\\$&")};var So=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g,ko={}.__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]},No=function(n,t){return t.querySelector(n)},Eo=function(n,t){return t.querySelectorAll(n)},Ao=function(n,t){var e=n.matches||n[x(n,"matchesSelector")];return(Ao=function(n,t){return e.call(n,t)})(n,t)};"function"==typeof Sizzle&&(No=function(n,t){return Sizzle(n,t)[0]||null},Eo=Sizzle,Ao=Sizzle.matchesSelector),ao.selection=function(){return ao.select(fo.documentElement)};var Co=ao.selection.prototype=[];Co.select=function(n){var t,e,r,i,u=[];n=A(n);for(var o=-1,a=this.length;++o=0&&"xmlns"!==(e=n.slice(0,t))&&(n=n.slice(t+1)),Lo.hasOwnProperty(e)?{space:Lo[e],local:n}:n}},Co.attr=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node();return n=ao.ns.qualify(n),n.local?e.getAttributeNS(n.space,n.local):e.getAttribute(n)}for(t in n)this.each(z(t,n[t]));return this}return this.each(z(n,t))},Co.classed=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node(),r=(n=T(n)).length,i=-1;if(t=e.classList){for(;++ii){if("string"!=typeof n){2>i&&(e="");for(r in n)this.each(P(r,n[r],e));return this}if(2>i){var u=this.node();return t(u).getComputedStyle(u,null).getPropertyValue(n)}r=""}return this.each(P(n,e,r))},Co.property=function(n,t){if(arguments.length<2){if("string"==typeof n)return this.node()[n];for(t in n)this.each(U(t,n[t]));return this}return this.each(U(n,t))},Co.text=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.textContent=null==t?"":t}:null==n?function(){this.textContent=""}:function(){this.textContent=n}):this.node().textContent},Co.html=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.innerHTML=null==t?"":t}:null==n?function(){this.innerHTML=""}:function(){this.innerHTML=n}):this.node().innerHTML},Co.append=function(n){return n=j(n),this.select(function(){return this.appendChild(n.apply(this,arguments))})},Co.insert=function(n,t){return n=j(n),t=A(t),this.select(function(){return this.insertBefore(n.apply(this,arguments),t.apply(this,arguments)||null)})},Co.remove=function(){return this.each(F)},Co.data=function(n,t){function e(n,e){var r,i,u,o=n.length,s=e.length,h=Math.min(o,s),p=new Array(s),g=new Array(s),v=new Array(o);if(t){var d,y=new c,m=new Array(o);for(r=-1;++rr;++r)g[r]=H(e[r]);for(;o>r;++r)v[r]=n[r]}g.update=p,g.parentNode=p.parentNode=v.parentNode=n.parentNode,a.push(g),l.push(p),f.push(v)}var r,i,u=-1,o=this.length;if(!arguments.length){for(n=new Array(o=(r=this[0]).length);++uu;u++){i.push(t=[]),t.parentNode=(e=this[u]).parentNode;for(var a=0,l=e.length;l>a;a++)(r=e[a])&&n.call(r,r.__data__,a,u)&&t.push(r)}return E(i)},Co.order=function(){for(var n=-1,t=this.length;++n=0;)(e=r[i])&&(u&&u!==e.nextSibling&&u.parentNode.insertBefore(e,u),u=e);return this},Co.sort=function(n){n=I.apply(this,arguments);for(var t=-1,e=this.length;++tn;n++)for(var e=this[n],r=0,i=e.length;i>r;r++){var u=e[r];if(u)return u}return null},Co.size=function(){var n=0;return Y(this,function(){++n}),n};var qo=[];ao.selection.enter=Z,ao.selection.enter.prototype=qo,qo.append=Co.append,qo.empty=Co.empty,qo.node=Co.node,qo.call=Co.call,qo.size=Co.size,qo.select=function(n){for(var t,e,r,i,u,o=[],a=-1,l=this.length;++ar){if("string"!=typeof n){2>r&&(t=!1);for(e in n)this.each(X(e,n[e],t));return this}if(2>r)return(r=this.node()["__on"+n])&&r._;e=!1}return this.each(X(n,t,e))};var To=ao.map({mouseenter:"mouseover",mouseleave:"mouseout"});fo&&To.forEach(function(n){"on"+n in fo&&To.remove(n)});var Ro,Do=0;ao.mouse=function(n){return J(n,k())};var Po=this.navigator&&/WebKit/.test(this.navigator.userAgent)?-1:0;ao.touch=function(n,t,e){if(arguments.length<3&&(e=t,t=k().changedTouches),t)for(var r,i=0,u=t.length;u>i;++i)if((r=t[i]).identifier===e)return J(n,r)},ao.behavior.drag=function(){function n(){this.on("mousedown.drag",u).on("touchstart.drag",o)}function e(n,t,e,u,o){return function(){function a(){var n,e,r=t(h,v);r&&(n=r[0]-M[0],e=r[1]-M[1],g|=n|e,M=r,p({type:"drag",x:r[0]+c[0],y:r[1]+c[1],dx:n,dy:e}))}function l(){t(h,v)&&(y.on(u+d,null).on(o+d,null),m(g),p({type:"dragend"}))}var c,f=this,s=ao.event.target.correspondingElement||ao.event.target,h=f.parentNode,p=r.of(f,arguments),g=0,v=n(),d=".drag"+(null==v?"":"-"+v),y=ao.select(e(s)).on(u+d,a).on(o+d,l),m=W(s),M=t(h,v);i?(c=i.apply(f,arguments),c=[c.x-M[0],c.y-M[1]]):c=[0,0],p({type:"dragstart"})}}var r=N(n,"drag","dragstart","dragend"),i=null,u=e(b,ao.mouse,t,"mousemove","mouseup"),o=e(G,ao.touch,m,"touchmove","touchend");return n.origin=function(t){return arguments.length?(i=t,n):i},ao.rebind(n,r,"on")},ao.touches=function(n,t){return arguments.length<2&&(t=k().touches),t?co(t).map(function(t){var e=J(n,t);return e.identifier=t.identifier,e}):[]};var Uo=1e-6,jo=Uo*Uo,Fo=Math.PI,Ho=2*Fo,Oo=Ho-Uo,Io=Fo/2,Yo=Fo/180,Zo=180/Fo,Vo=Math.SQRT2,Xo=2,$o=4;ao.interpolateZoom=function(n,t){var e,r,i=n[0],u=n[1],o=n[2],a=t[0],l=t[1],c=t[2],f=a-i,s=l-u,h=f*f+s*s;if(jo>h)r=Math.log(c/o)/Vo,e=function(n){return[i+n*f,u+n*s,o*Math.exp(Vo*n*r)]};else{var p=Math.sqrt(h),g=(c*c-o*o+$o*h)/(2*o*Xo*p),v=(c*c-o*o-$o*h)/(2*c*Xo*p),d=Math.log(Math.sqrt(g*g+1)-g),y=Math.log(Math.sqrt(v*v+1)-v);r=(y-d)/Vo,e=function(n){var t=n*r,e=rn(d),a=o/(Xo*p)*(e*un(Vo*t+d)-en(d));return[i+a*f,u+a*s,o*e/rn(Vo*t+d)]}}return e.duration=1e3*r,e},ao.behavior.zoom=function(){function n(n){n.on(L,s).on(Wo+".zoom",p).on("dblclick.zoom",g).on(R,h)}function e(n){return[(n[0]-k.x)/k.k,(n[1]-k.y)/k.k]}function r(n){return[n[0]*k.k+k.x,n[1]*k.k+k.y]}function i(n){k.k=Math.max(A[0],Math.min(A[1],n))}function u(n,t){t=r(t),k.x+=n[0]-t[0],k.y+=n[1]-t[1]}function o(t,e,r,o){t.__chart__={x:k.x,y:k.y,k:k.k},i(Math.pow(2,o)),u(d=e,r),t=ao.select(t),C>0&&(t=t.transition().duration(C)),t.call(n.event)}function a(){b&&b.domain(x.range().map(function(n){return(n-k.x)/k.k}).map(x.invert)),w&&w.domain(_.range().map(function(n){return(n-k.y)/k.k}).map(_.invert))}function l(n){z++||n({type:"zoomstart"})}function c(n){a(),n({type:"zoom",scale:k.k,translate:[k.x,k.y]})}function f(n){--z||(n({type:"zoomend"}),d=null)}function s(){function n(){a=1,u(ao.mouse(i),h),c(o)}function r(){s.on(q,null).on(T,null),p(a),f(o)}var i=this,o=D.of(i,arguments),a=0,s=ao.select(t(i)).on(q,n).on(T,r),h=e(ao.mouse(i)),p=W(i);Il.call(i),l(o)}function h(){function n(){var n=ao.touches(g);return p=k.k,n.forEach(function(n){n.identifier in d&&(d[n.identifier]=e(n))}),n}function t(){var t=ao.event.target;ao.select(t).on(x,r).on(b,a),_.push(t);for(var e=ao.event.changedTouches,i=0,u=e.length;u>i;++i)d[e[i].identifier]=null;var l=n(),c=Date.now();if(1===l.length){if(500>c-M){var f=l[0];o(g,f,d[f.identifier],Math.floor(Math.log(k.k)/Math.LN2)+1),S()}M=c}else if(l.length>1){var f=l[0],s=l[1],h=f[0]-s[0],p=f[1]-s[1];y=h*h+p*p}}function r(){var n,t,e,r,o=ao.touches(g);Il.call(g);for(var a=0,l=o.length;l>a;++a,r=null)if(e=o[a],r=d[e.identifier]){if(t)break;n=e,t=r}if(r){var f=(f=e[0]-n[0])*f+(f=e[1]-n[1])*f,s=y&&Math.sqrt(f/y);n=[(n[0]+e[0])/2,(n[1]+e[1])/2],t=[(t[0]+r[0])/2,(t[1]+r[1])/2],i(s*p)}M=null,u(n,t),c(v)}function a(){if(ao.event.touches.length){for(var t=ao.event.changedTouches,e=0,r=t.length;r>e;++e)delete d[t[e].identifier];for(var i in d)return void n()}ao.selectAll(_).on(m,null),w.on(L,s).on(R,h),N(),f(v)}var p,g=this,v=D.of(g,arguments),d={},y=0,m=".zoom-"+ao.event.changedTouches[0].identifier,x="touchmove"+m,b="touchend"+m,_=[],w=ao.select(g),N=W(g);t(),l(v),w.on(L,null).on(R,t)}function p(){var n=D.of(this,arguments);m?clearTimeout(m):(Il.call(this),v=e(d=y||ao.mouse(this)),l(n)),m=setTimeout(function(){m=null,f(n)},50),S(),i(Math.pow(2,.002*Bo())*k.k),u(d,v),c(n)}function g(){var n=ao.mouse(this),t=Math.log(k.k)/Math.LN2;o(this,n,e(n),ao.event.shiftKey?Math.ceil(t)-1:Math.floor(t)+1)}var v,d,y,m,M,x,b,_,w,k={x:0,y:0,k:1},E=[960,500],A=Jo,C=250,z=0,L="mousedown.zoom",q="mousemove.zoom",T="mouseup.zoom",R="touchstart.zoom",D=N(n,"zoomstart","zoom","zoomend");return Wo||(Wo="onwheel"in fo?(Bo=function(){return-ao.event.deltaY*(ao.event.deltaMode?120:1)},"wheel"):"onmousewheel"in fo?(Bo=function(){return ao.event.wheelDelta},"mousewheel"):(Bo=function(){return-ao.event.detail},"MozMousePixelScroll")),n.event=function(n){n.each(function(){var n=D.of(this,arguments),t=k;Hl?ao.select(this).transition().each("start.zoom",function(){k=this.__chart__||{x:0,y:0,k:1},l(n)}).tween("zoom:zoom",function(){var e=E[0],r=E[1],i=d?d[0]:e/2,u=d?d[1]:r/2,o=ao.interpolateZoom([(i-k.x)/k.k,(u-k.y)/k.k,e/k.k],[(i-t.x)/t.k,(u-t.y)/t.k,e/t.k]);return function(t){var r=o(t),a=e/r[2];this.__chart__=k={x:i-r[0]*a,y:u-r[1]*a,k:a},c(n)}}).each("interrupt.zoom",function(){f(n)}).each("end.zoom",function(){f(n)}):(this.__chart__=k,l(n),c(n),f(n))})},n.translate=function(t){return arguments.length?(k={x:+t[0],y:+t[1],k:k.k},a(),n):[k.x,k.y]},n.scale=function(t){return arguments.length?(k={x:k.x,y:k.y,k:null},i(+t),a(),n):k.k},n.scaleExtent=function(t){return arguments.length?(A=null==t?Jo:[+t[0],+t[1]],n):A},n.center=function(t){return arguments.length?(y=t&&[+t[0],+t[1]],n):y},n.size=function(t){return arguments.length?(E=t&&[+t[0],+t[1]],n):E},n.duration=function(t){return arguments.length?(C=+t,n):C},n.x=function(t){return arguments.length?(b=t,x=t.copy(),k={x:0,y:0,k:1},n):b},n.y=function(t){return arguments.length?(w=t,_=t.copy(),k={x:0,y:0,k:1},n):w},ao.rebind(n,D,"on")};var Bo,Wo,Jo=[0,1/0];ao.color=an,an.prototype.toString=function(){return this.rgb()+""},ao.hsl=ln;var Go=ln.prototype=new an;Go.brighter=function(n){return n=Math.pow(.7,arguments.length?n:1),new ln(this.h,this.s,this.l/n)},Go.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new ln(this.h,this.s,n*this.l)},Go.rgb=function(){return cn(this.h,this.s,this.l)},ao.hcl=fn;var Ko=fn.prototype=new an;Ko.brighter=function(n){return new fn(this.h,this.c,Math.min(100,this.l+Qo*(arguments.length?n:1)))},Ko.darker=function(n){return new fn(this.h,this.c,Math.max(0,this.l-Qo*(arguments.length?n:1)))},Ko.rgb=function(){return sn(this.h,this.c,this.l).rgb()},ao.lab=hn;var Qo=18,na=.95047,ta=1,ea=1.08883,ra=hn.prototype=new an;ra.brighter=function(n){return new hn(Math.min(100,this.l+Qo*(arguments.length?n:1)),this.a,this.b)},ra.darker=function(n){return new hn(Math.max(0,this.l-Qo*(arguments.length?n:1)),this.a,this.b)},ra.rgb=function(){return pn(this.l,this.a,this.b)},ao.rgb=mn;var ia=mn.prototype=new an;ia.brighter=function(n){n=Math.pow(.7,arguments.length?n:1);var t=this.r,e=this.g,r=this.b,i=30;return t||e||r?(t&&i>t&&(t=i),e&&i>e&&(e=i),r&&i>r&&(r=i),new mn(Math.min(255,t/n),Math.min(255,e/n),Math.min(255,r/n))):new mn(i,i,i)},ia.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new mn(n*this.r,n*this.g,n*this.b)},ia.hsl=function(){return wn(this.r,this.g,this.b)},ia.toString=function(){return"#"+bn(this.r)+bn(this.g)+bn(this.b)};var ua=ao.map({aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074});ua.forEach(function(n,t){ua.set(n,Mn(t))}),ao.functor=En,ao.xhr=An(m),ao.dsv=function(n,t){function e(n,e,u){arguments.length<3&&(u=e,e=null);var o=Cn(n,t,null==e?r:i(e),u);return o.row=function(n){return arguments.length?o.response(null==(e=n)?r:i(n)):e},o}function r(n){return e.parse(n.responseText)}function i(n){return function(t){return e.parse(t.responseText,n)}}function u(t){return t.map(o).join(n)}function o(n){return a.test(n)?'"'+n.replace(/\"/g,'""')+'"':n}var a=new RegExp('["'+n+"\n]"),l=n.charCodeAt(0);return e.parse=function(n,t){var r;return e.parseRows(n,function(n,e){if(r)return r(n,e-1);var i=new Function("d","return {"+n.map(function(n,t){return JSON.stringify(n)+": d["+t+"]"}).join(",")+"}");r=t?function(n,e){return t(i(n),e)}:i})},e.parseRows=function(n,t){function e(){if(f>=c)return o;if(i)return i=!1,u;var t=f;if(34===n.charCodeAt(t)){for(var e=t;e++f;){var r=n.charCodeAt(f++),a=1;if(10===r)i=!0;else if(13===r)i=!0,10===n.charCodeAt(f)&&(++f,++a);else if(r!==l)continue;return n.slice(t,f-a)}return n.slice(t)}for(var r,i,u={},o={},a=[],c=n.length,f=0,s=0;(r=e())!==o;){for(var h=[];r!==u&&r!==o;)h.push(r),r=e();t&&null==(h=t(h,s++))||a.push(h)}return a},e.format=function(t){if(Array.isArray(t[0]))return e.formatRows(t);var r=new y,i=[];return t.forEach(function(n){for(var t in n)r.has(t)||i.push(r.add(t))}),[i.map(o).join(n)].concat(t.map(function(t){return i.map(function(n){return o(t[n])}).join(n)})).join("\n")},e.formatRows=function(n){return n.map(u).join("\n")},e},ao.csv=ao.dsv(",","text/csv"),ao.tsv=ao.dsv(" ","text/tab-separated-values");var oa,aa,la,ca,fa=this[x(this,"requestAnimationFrame")]||function(n){setTimeout(n,17)};ao.timer=function(){qn.apply(this,arguments)},ao.timer.flush=function(){Rn(),Dn()},ao.round=function(n,t){return t?Math.round(n*(t=Math.pow(10,t)))/t:Math.round(n)};var sa=["y","z","a","f","p","n","\xb5","m","","k","M","G","T","P","E","Z","Y"].map(Un);ao.formatPrefix=function(n,t){var e=0;return(n=+n)&&(0>n&&(n*=-1),t&&(n=ao.round(n,Pn(n,t))),e=1+Math.floor(1e-12+Math.log(n)/Math.LN10),e=Math.max(-24,Math.min(24,3*Math.floor((e-1)/3)))),sa[8+e/3]};var ha=/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,pa=ao.map({b:function(n){return n.toString(2)},c:function(n){return String.fromCharCode(n)},o:function(n){return n.toString(8)},x:function(n){return n.toString(16)},X:function(n){return n.toString(16).toUpperCase()},g:function(n,t){return n.toPrecision(t)},e:function(n,t){return n.toExponential(t)},f:function(n,t){return n.toFixed(t)},r:function(n,t){return(n=ao.round(n,Pn(n,t))).toFixed(Math.max(0,Math.min(20,Pn(n*(1+1e-15),t))))}}),ga=ao.time={},va=Date;Hn.prototype={getDate:function(){return this._.getUTCDate()},getDay:function(){return this._.getUTCDay()},getFullYear:function(){return this._.getUTCFullYear()},getHours:function(){return this._.getUTCHours()},getMilliseconds:function(){return this._.getUTCMilliseconds()},getMinutes:function(){return this._.getUTCMinutes()},getMonth:function(){return this._.getUTCMonth()},getSeconds:function(){return this._.getUTCSeconds()},getTime:function(){return this._.getTime()},getTimezoneOffset:function(){return 0},valueOf:function(){return this._.valueOf()},setDate:function(){da.setUTCDate.apply(this._,arguments)},setDay:function(){da.setUTCDay.apply(this._,arguments)},setFullYear:function(){da.setUTCFullYear.apply(this._,arguments)},setHours:function(){da.setUTCHours.apply(this._,arguments)},setMilliseconds:function(){da.setUTCMilliseconds.apply(this._,arguments)},setMinutes:function(){da.setUTCMinutes.apply(this._,arguments)},setMonth:function(){da.setUTCMonth.apply(this._,arguments)},setSeconds:function(){da.setUTCSeconds.apply(this._,arguments)},setTime:function(){da.setTime.apply(this._,arguments)}};var da=Date.prototype;ga.year=On(function(n){return n=ga.day(n),n.setMonth(0,1),n},function(n,t){n.setFullYear(n.getFullYear()+t)},function(n){return n.getFullYear()}),ga.years=ga.year.range,ga.years.utc=ga.year.utc.range,ga.day=On(function(n){var t=new va(2e3,0);return t.setFullYear(n.getFullYear(),n.getMonth(),n.getDate()),t},function(n,t){n.setDate(n.getDate()+t)},function(n){return n.getDate()-1}),ga.days=ga.day.range,ga.days.utc=ga.day.utc.range,ga.dayOfYear=function(n){var t=ga.year(n);return Math.floor((n-t-6e4*(n.getTimezoneOffset()-t.getTimezoneOffset()))/864e5)},["sunday","monday","tuesday","wednesday","thursday","friday","saturday"].forEach(function(n,t){t=7-t;var e=ga[n]=On(function(n){return(n=ga.day(n)).setDate(n.getDate()-(n.getDay()+t)%7),n},function(n,t){n.setDate(n.getDate()+7*Math.floor(t))},function(n){var e=ga.year(n).getDay();return Math.floor((ga.dayOfYear(n)+(e+t)%7)/7)-(e!==t)});ga[n+"s"]=e.range,ga[n+"s"].utc=e.utc.range,ga[n+"OfYear"]=function(n){var e=ga.year(n).getDay();return Math.floor((ga.dayOfYear(n)+(e+t)%7)/7)}}),ga.week=ga.sunday,ga.weeks=ga.sunday.range,ga.weeks.utc=ga.sunday.utc.range,ga.weekOfYear=ga.sundayOfYear;var ya={"-":"",_:" ",0:"0"},ma=/^\s*\d+/,Ma=/^%/;ao.locale=function(n){return{numberFormat:jn(n),timeFormat:Yn(n)}};var xa=ao.locale({decimal:".",thousands:",",grouping:[3],currency:["$",""],dateTime:"%a %b %e %X %Y",date:"%m/%d/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"], +shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});ao.format=xa.numberFormat,ao.geo={},ft.prototype={s:0,t:0,add:function(n){st(n,this.t,ba),st(ba.s,this.s,this),this.s?this.t+=ba.t:this.s=ba.t},reset:function(){this.s=this.t=0},valueOf:function(){return this.s}};var ba=new ft;ao.geo.stream=function(n,t){n&&_a.hasOwnProperty(n.type)?_a[n.type](n,t):ht(n,t)};var _a={Feature:function(n,t){ht(n.geometry,t)},FeatureCollection:function(n,t){for(var e=n.features,r=-1,i=e.length;++rn?4*Fo+n:n,Na.lineStart=Na.lineEnd=Na.point=b}};ao.geo.bounds=function(){function n(n,t){M.push(x=[f=n,h=n]),s>t&&(s=t),t>p&&(p=t)}function t(t,e){var r=dt([t*Yo,e*Yo]);if(y){var i=mt(y,r),u=[i[1],-i[0],0],o=mt(u,i);bt(o),o=_t(o);var l=t-g,c=l>0?1:-1,v=o[0]*Zo*c,d=xo(l)>180;if(d^(v>c*g&&c*t>v)){var m=o[1]*Zo;m>p&&(p=m)}else if(v=(v+360)%360-180,d^(v>c*g&&c*t>v)){var m=-o[1]*Zo;s>m&&(s=m)}else s>e&&(s=e),e>p&&(p=e);d?g>t?a(f,t)>a(f,h)&&(h=t):a(t,h)>a(f,h)&&(f=t):h>=f?(f>t&&(f=t),t>h&&(h=t)):t>g?a(f,t)>a(f,h)&&(h=t):a(t,h)>a(f,h)&&(f=t)}else n(t,e);y=r,g=t}function e(){b.point=t}function r(){x[0]=f,x[1]=h,b.point=n,y=null}function i(n,e){if(y){var r=n-g;m+=xo(r)>180?r+(r>0?360:-360):r}else v=n,d=e;Na.point(n,e),t(n,e)}function u(){Na.lineStart()}function o(){i(v,d),Na.lineEnd(),xo(m)>Uo&&(f=-(h=180)),x[0]=f,x[1]=h,y=null}function a(n,t){return(t-=n)<0?t+360:t}function l(n,t){return n[0]-t[0]}function c(n,t){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:nka?(f=-(h=180),s=-(p=90)):m>Uo?p=90:-Uo>m&&(s=-90),x[0]=f,x[1]=h}};return function(n){p=h=-(f=s=1/0),M=[],ao.geo.stream(n,b);var t=M.length;if(t){M.sort(l);for(var e,r=1,i=M[0],u=[i];t>r;++r)e=M[r],c(e[0],i)||c(e[1],i)?(a(i[0],e[1])>a(i[0],i[1])&&(i[1]=e[1]),a(e[0],i[1])>a(i[0],i[1])&&(i[0]=e[0])):u.push(i=e);for(var o,e,g=-(1/0),t=u.length-1,r=0,i=u[t];t>=r;i=e,++r)e=u[r],(o=a(i[1],e[0]))>g&&(g=o,f=e[0],h=i[1])}return M=x=null,f===1/0||s===1/0?[[NaN,NaN],[NaN,NaN]]:[[f,s],[h,p]]}}(),ao.geo.centroid=function(n){Ea=Aa=Ca=za=La=qa=Ta=Ra=Da=Pa=Ua=0,ao.geo.stream(n,ja);var t=Da,e=Pa,r=Ua,i=t*t+e*e+r*r;return jo>i&&(t=qa,e=Ta,r=Ra,Uo>Aa&&(t=Ca,e=za,r=La),i=t*t+e*e+r*r,jo>i)?[NaN,NaN]:[Math.atan2(e,t)*Zo,tn(r/Math.sqrt(i))*Zo]};var Ea,Aa,Ca,za,La,qa,Ta,Ra,Da,Pa,Ua,ja={sphere:b,point:St,lineStart:Nt,lineEnd:Et,polygonStart:function(){ja.lineStart=At},polygonEnd:function(){ja.lineStart=Nt}},Fa=Rt(zt,jt,Ht,[-Fo,-Fo/2]),Ha=1e9;ao.geo.clipExtent=function(){var n,t,e,r,i,u,o={stream:function(n){return i&&(i.valid=!1),i=u(n),i.valid=!0,i},extent:function(a){return arguments.length?(u=Zt(n=+a[0][0],t=+a[0][1],e=+a[1][0],r=+a[1][1]),i&&(i.valid=!1,i=null),o):[[n,t],[e,r]]}};return o.extent([[0,0],[960,500]])},(ao.geo.conicEqualArea=function(){return Vt(Xt)}).raw=Xt,ao.geo.albers=function(){return ao.geo.conicEqualArea().rotate([96,0]).center([-.6,38.7]).parallels([29.5,45.5]).scale(1070)},ao.geo.albersUsa=function(){function n(n){var u=n[0],o=n[1];return t=null,e(u,o),t||(r(u,o),t)||i(u,o),t}var t,e,r,i,u=ao.geo.albers(),o=ao.geo.conicEqualArea().rotate([154,0]).center([-2,58.5]).parallels([55,65]),a=ao.geo.conicEqualArea().rotate([157,0]).center([-3,19.9]).parallels([8,18]),l={point:function(n,e){t=[n,e]}};return n.invert=function(n){var t=u.scale(),e=u.translate(),r=(n[0]-e[0])/t,i=(n[1]-e[1])/t;return(i>=.12&&.234>i&&r>=-.425&&-.214>r?o:i>=.166&&.234>i&&r>=-.214&&-.115>r?a:u).invert(n)},n.stream=function(n){var t=u.stream(n),e=o.stream(n),r=a.stream(n);return{point:function(n,i){t.point(n,i),e.point(n,i),r.point(n,i)},sphere:function(){t.sphere(),e.sphere(),r.sphere()},lineStart:function(){t.lineStart(),e.lineStart(),r.lineStart()},lineEnd:function(){t.lineEnd(),e.lineEnd(),r.lineEnd()},polygonStart:function(){t.polygonStart(),e.polygonStart(),r.polygonStart()},polygonEnd:function(){t.polygonEnd(),e.polygonEnd(),r.polygonEnd()}}},n.precision=function(t){return arguments.length?(u.precision(t),o.precision(t),a.precision(t),n):u.precision()},n.scale=function(t){return arguments.length?(u.scale(t),o.scale(.35*t),a.scale(t),n.translate(u.translate())):u.scale()},n.translate=function(t){if(!arguments.length)return u.translate();var c=u.scale(),f=+t[0],s=+t[1];return e=u.translate(t).clipExtent([[f-.455*c,s-.238*c],[f+.455*c,s+.238*c]]).stream(l).point,r=o.translate([f-.307*c,s+.201*c]).clipExtent([[f-.425*c+Uo,s+.12*c+Uo],[f-.214*c-Uo,s+.234*c-Uo]]).stream(l).point,i=a.translate([f-.205*c,s+.212*c]).clipExtent([[f-.214*c+Uo,s+.166*c+Uo],[f-.115*c-Uo,s+.234*c-Uo]]).stream(l).point,n},n.scale(1070)};var Oa,Ia,Ya,Za,Va,Xa,$a={point:b,lineStart:b,lineEnd:b,polygonStart:function(){Ia=0,$a.lineStart=$t},polygonEnd:function(){$a.lineStart=$a.lineEnd=$a.point=b,Oa+=xo(Ia/2)}},Ba={point:Bt,lineStart:b,lineEnd:b,polygonStart:b,polygonEnd:b},Wa={point:Gt,lineStart:Kt,lineEnd:Qt,polygonStart:function(){Wa.lineStart=ne},polygonEnd:function(){Wa.point=Gt,Wa.lineStart=Kt,Wa.lineEnd=Qt}};ao.geo.path=function(){function n(n){return n&&("function"==typeof a&&u.pointRadius(+a.apply(this,arguments)),o&&o.valid||(o=i(u)),ao.geo.stream(n,o)),u.result()}function t(){return o=null,n}var e,r,i,u,o,a=4.5;return n.area=function(n){return Oa=0,ao.geo.stream(n,i($a)),Oa},n.centroid=function(n){return Ca=za=La=qa=Ta=Ra=Da=Pa=Ua=0,ao.geo.stream(n,i(Wa)),Ua?[Da/Ua,Pa/Ua]:Ra?[qa/Ra,Ta/Ra]:La?[Ca/La,za/La]:[NaN,NaN]},n.bounds=function(n){return Va=Xa=-(Ya=Za=1/0),ao.geo.stream(n,i(Ba)),[[Ya,Za],[Va,Xa]]},n.projection=function(n){return arguments.length?(i=(e=n)?n.stream||re(n):m,t()):e},n.context=function(n){return arguments.length?(u=null==(r=n)?new Wt:new te(n),"function"!=typeof a&&u.pointRadius(a),t()):r},n.pointRadius=function(t){return arguments.length?(a="function"==typeof t?t:(u.pointRadius(+t),+t),n):a},n.projection(ao.geo.albersUsa()).context(null)},ao.geo.transform=function(n){return{stream:function(t){var e=new ie(t);for(var r in n)e[r]=n[r];return e}}},ie.prototype={point:function(n,t){this.stream.point(n,t)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}},ao.geo.projection=oe,ao.geo.projectionMutator=ae,(ao.geo.equirectangular=function(){return oe(ce)}).raw=ce.invert=ce,ao.geo.rotation=function(n){function t(t){return t=n(t[0]*Yo,t[1]*Yo),t[0]*=Zo,t[1]*=Zo,t}return n=se(n[0]%360*Yo,n[1]*Yo,n.length>2?n[2]*Yo:0),t.invert=function(t){return t=n.invert(t[0]*Yo,t[1]*Yo),t[0]*=Zo,t[1]*=Zo,t},t},fe.invert=ce,ao.geo.circle=function(){function n(){var n="function"==typeof r?r.apply(this,arguments):r,t=se(-n[0]*Yo,-n[1]*Yo,0).invert,i=[];return e(null,null,1,{point:function(n,e){i.push(n=t(n,e)),n[0]*=Zo,n[1]*=Zo}}),{type:"Polygon",coordinates:[i]}}var t,e,r=[0,0],i=6;return n.origin=function(t){return arguments.length?(r=t,n):r},n.angle=function(r){return arguments.length?(e=ve((t=+r)*Yo,i*Yo),n):t},n.precision=function(r){return arguments.length?(e=ve(t*Yo,(i=+r)*Yo),n):i},n.angle(90)},ao.geo.distance=function(n,t){var e,r=(t[0]-n[0])*Yo,i=n[1]*Yo,u=t[1]*Yo,o=Math.sin(r),a=Math.cos(r),l=Math.sin(i),c=Math.cos(i),f=Math.sin(u),s=Math.cos(u);return Math.atan2(Math.sqrt((e=s*o)*e+(e=c*f-l*s*a)*e),l*f+c*s*a)},ao.geo.graticule=function(){function n(){return{type:"MultiLineString",coordinates:t()}}function t(){return ao.range(Math.ceil(u/d)*d,i,d).map(h).concat(ao.range(Math.ceil(c/y)*y,l,y).map(p)).concat(ao.range(Math.ceil(r/g)*g,e,g).filter(function(n){return xo(n%d)>Uo}).map(f)).concat(ao.range(Math.ceil(a/v)*v,o,v).filter(function(n){return xo(n%y)>Uo}).map(s))}var e,r,i,u,o,a,l,c,f,s,h,p,g=10,v=g,d=90,y=360,m=2.5;return n.lines=function(){return t().map(function(n){return{type:"LineString",coordinates:n}})},n.outline=function(){return{type:"Polygon",coordinates:[h(u).concat(p(l).slice(1),h(i).reverse().slice(1),p(c).reverse().slice(1))]}},n.extent=function(t){return arguments.length?n.majorExtent(t).minorExtent(t):n.minorExtent()},n.majorExtent=function(t){return arguments.length?(u=+t[0][0],i=+t[1][0],c=+t[0][1],l=+t[1][1],u>i&&(t=u,u=i,i=t),c>l&&(t=c,c=l,l=t),n.precision(m)):[[u,c],[i,l]]},n.minorExtent=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],a=+t[0][1],o=+t[1][1],r>e&&(t=r,r=e,e=t),a>o&&(t=a,a=o,o=t),n.precision(m)):[[r,a],[e,o]]},n.step=function(t){return arguments.length?n.majorStep(t).minorStep(t):n.minorStep()},n.majorStep=function(t){return arguments.length?(d=+t[0],y=+t[1],n):[d,y]},n.minorStep=function(t){return arguments.length?(g=+t[0],v=+t[1],n):[g,v]},n.precision=function(t){return arguments.length?(m=+t,f=ye(a,o,90),s=me(r,e,m),h=ye(c,l,90),p=me(u,i,m),n):m},n.majorExtent([[-180,-90+Uo],[180,90-Uo]]).minorExtent([[-180,-80-Uo],[180,80+Uo]])},ao.geo.greatArc=function(){function n(){return{type:"LineString",coordinates:[t||r.apply(this,arguments),e||i.apply(this,arguments)]}}var t,e,r=Me,i=xe;return n.distance=function(){return ao.geo.distance(t||r.apply(this,arguments),e||i.apply(this,arguments))},n.source=function(e){return arguments.length?(r=e,t="function"==typeof e?null:e,n):r},n.target=function(t){return arguments.length?(i=t,e="function"==typeof t?null:t,n):i},n.precision=function(){return arguments.length?n:0},n},ao.geo.interpolate=function(n,t){return be(n[0]*Yo,n[1]*Yo,t[0]*Yo,t[1]*Yo)},ao.geo.length=function(n){return Ja=0,ao.geo.stream(n,Ga),Ja};var Ja,Ga={sphere:b,point:b,lineStart:_e,lineEnd:b,polygonStart:b,polygonEnd:b},Ka=we(function(n){return Math.sqrt(2/(1+n))},function(n){return 2*Math.asin(n/2)});(ao.geo.azimuthalEqualArea=function(){return oe(Ka)}).raw=Ka;var Qa=we(function(n){var t=Math.acos(n);return t&&t/Math.sin(t)},m);(ao.geo.azimuthalEquidistant=function(){return oe(Qa)}).raw=Qa,(ao.geo.conicConformal=function(){return Vt(Se)}).raw=Se,(ao.geo.conicEquidistant=function(){return Vt(ke)}).raw=ke;var nl=we(function(n){return 1/n},Math.atan);(ao.geo.gnomonic=function(){return oe(nl)}).raw=nl,Ne.invert=function(n,t){return[n,2*Math.atan(Math.exp(t))-Io]},(ao.geo.mercator=function(){return Ee(Ne)}).raw=Ne;var tl=we(function(){return 1},Math.asin);(ao.geo.orthographic=function(){return oe(tl)}).raw=tl;var el=we(function(n){return 1/(1+n)},function(n){return 2*Math.atan(n)});(ao.geo.stereographic=function(){return oe(el)}).raw=el,Ae.invert=function(n,t){return[-t,2*Math.atan(Math.exp(n))-Io]},(ao.geo.transverseMercator=function(){var n=Ee(Ae),t=n.center,e=n.rotate;return n.center=function(n){return n?t([-n[1],n[0]]):(n=t(),[n[1],-n[0]])},n.rotate=function(n){return n?e([n[0],n[1],n.length>2?n[2]+90:90]):(n=e(),[n[0],n[1],n[2]-90])},e([0,0,90])}).raw=Ae,ao.geom={},ao.geom.hull=function(n){function t(n){if(n.length<3)return[];var t,i=En(e),u=En(r),o=n.length,a=[],l=[];for(t=0;o>t;t++)a.push([+i.call(this,n[t],t),+u.call(this,n[t],t),t]);for(a.sort(qe),t=0;o>t;t++)l.push([a[t][0],-a[t][1]]);var c=Le(a),f=Le(l),s=f[0]===c[0],h=f[f.length-1]===c[c.length-1],p=[];for(t=c.length-1;t>=0;--t)p.push(n[a[c[t]][2]]);for(t=+s;t=r&&c.x<=u&&c.y>=i&&c.y<=o?[[r,o],[u,o],[u,i],[r,i]]:[];f.point=n[a]}),t}function e(n){return n.map(function(n,t){return{x:Math.round(u(n,t)/Uo)*Uo,y:Math.round(o(n,t)/Uo)*Uo,i:t}})}var r=Ce,i=ze,u=r,o=i,a=sl;return n?t(n):(t.links=function(n){return ar(e(n)).edges.filter(function(n){return n.l&&n.r}).map(function(t){return{source:n[t.l.i],target:n[t.r.i]}})},t.triangles=function(n){var t=[];return ar(e(n)).cells.forEach(function(e,r){for(var i,u,o=e.site,a=e.edges.sort(Ve),l=-1,c=a.length,f=a[c-1].edge,s=f.l===o?f.r:f.l;++l=c,h=r>=f,p=h<<1|s;n.leaf=!1,n=n.nodes[p]||(n.nodes[p]=hr()),s?i=c:a=c,h?o=f:l=f,u(n,t,e,r,i,o,a,l)}var f,s,h,p,g,v,d,y,m,M=En(a),x=En(l);if(null!=t)v=t,d=e,y=r,m=i;else if(y=m=-(v=d=1/0),s=[],h=[],g=n.length,o)for(p=0;g>p;++p)f=n[p],f.xy&&(y=f.x),f.y>m&&(m=f.y),s.push(f.x),h.push(f.y);else for(p=0;g>p;++p){var b=+M(f=n[p],p),_=+x(f,p);v>b&&(v=b),d>_&&(d=_),b>y&&(y=b),_>m&&(m=_),s.push(b),h.push(_)}var w=y-v,S=m-d;w>S?m=d+w:y=v+S;var k=hr();if(k.add=function(n){u(k,n,+M(n,++p),+x(n,p),v,d,y,m)},k.visit=function(n){pr(n,k,v,d,y,m)},k.find=function(n){return gr(k,n[0],n[1],v,d,y,m)},p=-1,null==t){for(;++p=0?n.slice(0,t):n,r=t>=0?n.slice(t+1):"in";return e=vl.get(e)||gl,r=dl.get(r)||m,br(r(e.apply(null,lo.call(arguments,1))))},ao.interpolateHcl=Rr,ao.interpolateHsl=Dr,ao.interpolateLab=Pr,ao.interpolateRound=Ur,ao.transform=function(n){var t=fo.createElementNS(ao.ns.prefix.svg,"g");return(ao.transform=function(n){if(null!=n){t.setAttribute("transform",n);var e=t.transform.baseVal.consolidate()}return new jr(e?e.matrix:yl)})(n)},jr.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var yl={a:1,b:0,c:0,d:1,e:0,f:0};ao.interpolateTransform=$r,ao.layout={},ao.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++ea*a/y){if(v>l){var c=t.charge/l;n.px-=u*c,n.py-=o*c}return!0}if(t.point&&l&&v>l){var c=t.pointCharge/l;n.px-=u*c,n.py-=o*c}}return!t.charge}}function t(n){n.px=ao.event.x,n.py=ao.event.y,l.resume()}var e,r,i,u,o,a,l={},c=ao.dispatch("start","tick","end"),f=[1,1],s=.9,h=ml,p=Ml,g=-30,v=xl,d=.1,y=.64,M=[],x=[];return l.tick=function(){if((i*=.99)<.005)return e=null,c.end({type:"end",alpha:i=0}),!0;var t,r,l,h,p,v,y,m,b,_=M.length,w=x.length;for(r=0;w>r;++r)l=x[r],h=l.source,p=l.target,m=p.x-h.x,b=p.y-h.y,(v=m*m+b*b)&&(v=i*o[r]*((v=Math.sqrt(v))-u[r])/v,m*=v,b*=v,p.x-=m*(y=h.weight+p.weight?h.weight/(h.weight+p.weight):.5),p.y-=b*y,h.x+=m*(y=1-y),h.y+=b*y);if((y=i*d)&&(m=f[0]/2,b=f[1]/2,r=-1,y))for(;++r<_;)l=M[r],l.x+=(m-l.x)*y,l.y+=(b-l.y)*y;if(g)for(ri(t=ao.geom.quadtree(M),i,a),r=-1;++r<_;)(l=M[r]).fixed||t.visit(n(l));for(r=-1;++r<_;)l=M[r],l.fixed?(l.x=l.px,l.y=l.py):(l.x-=(l.px-(l.px=l.x))*s,l.y-=(l.py-(l.py=l.y))*s);c.tick({type:"tick",alpha:i})},l.nodes=function(n){return arguments.length?(M=n,l):M},l.links=function(n){return arguments.length?(x=n,l):x},l.size=function(n){return arguments.length?(f=n,l):f},l.linkDistance=function(n){return arguments.length?(h="function"==typeof n?n:+n,l):h},l.distance=l.linkDistance,l.linkStrength=function(n){return arguments.length?(p="function"==typeof n?n:+n,l):p},l.friction=function(n){return arguments.length?(s=+n,l):s},l.charge=function(n){return arguments.length?(g="function"==typeof n?n:+n,l):g},l.chargeDistance=function(n){return arguments.length?(v=n*n,l):Math.sqrt(v)},l.gravity=function(n){return arguments.length?(d=+n,l):d},l.theta=function(n){return arguments.length?(y=n*n,l):Math.sqrt(y)},l.alpha=function(n){return arguments.length?(n=+n,i?n>0?i=n:(e.c=null,e.t=NaN,e=null,c.end({type:"end",alpha:i=0})):n>0&&(c.start({type:"start",alpha:i=n}),e=qn(l.tick)),l):i},l.start=function(){function n(n,r){if(!e){for(e=new Array(i),l=0;i>l;++l)e[l]=[];for(l=0;c>l;++l){var u=x[l];e[u.source.index].push(u.target),e[u.target.index].push(u.source)}}for(var o,a=e[t],l=-1,f=a.length;++lt;++t)(r=M[t]).index=t,r.weight=0;for(t=0;c>t;++t)r=x[t],"number"==typeof r.source&&(r.source=M[r.source]),"number"==typeof r.target&&(r.target=M[r.target]),++r.source.weight,++r.target.weight;for(t=0;i>t;++t)r=M[t],isNaN(r.x)&&(r.x=n("x",s)),isNaN(r.y)&&(r.y=n("y",v)),isNaN(r.px)&&(r.px=r.x),isNaN(r.py)&&(r.py=r.y);if(u=[],"function"==typeof h)for(t=0;c>t;++t)u[t]=+h.call(this,x[t],t);else for(t=0;c>t;++t)u[t]=h;if(o=[],"function"==typeof p)for(t=0;c>t;++t)o[t]=+p.call(this,x[t],t);else for(t=0;c>t;++t)o[t]=p;if(a=[],"function"==typeof g)for(t=0;i>t;++t)a[t]=+g.call(this,M[t],t);else for(t=0;i>t;++t)a[t]=g;return l.resume()},l.resume=function(){return l.alpha(.1)},l.stop=function(){return l.alpha(0)},l.drag=function(){return r||(r=ao.behavior.drag().origin(m).on("dragstart.force",Qr).on("drag.force",t).on("dragend.force",ni)),arguments.length?void this.on("mouseover.force",ti).on("mouseout.force",ei).call(r):r},ao.rebind(l,c,"on")};var ml=20,Ml=1,xl=1/0;ao.layout.hierarchy=function(){function n(i){var u,o=[i],a=[];for(i.depth=0;null!=(u=o.pop());)if(a.push(u),(c=e.call(n,u,u.depth))&&(l=c.length)){for(var l,c,f;--l>=0;)o.push(f=c[l]),f.parent=u,f.depth=u.depth+1;r&&(u.value=0),u.children=c}else r&&(u.value=+r.call(n,u,u.depth)||0),delete u.children;return oi(i,function(n){var e,i;t&&(e=n.children)&&e.sort(t),r&&(i=n.parent)&&(i.value+=n.value)}),a}var t=ci,e=ai,r=li;return n.sort=function(e){return arguments.length?(t=e,n):t},n.children=function(t){return arguments.length?(e=t,n):e},n.value=function(t){return arguments.length?(r=t,n):r},n.revalue=function(t){return r&&(ui(t,function(n){n.children&&(n.value=0)}),oi(t,function(t){var e;t.children||(t.value=+r.call(n,t,t.depth)||0),(e=t.parent)&&(e.value+=t.value)})),t},n},ao.layout.partition=function(){function n(t,e,r,i){var u=t.children;if(t.x=e,t.y=t.depth*i,t.dx=r,t.dy=i,u&&(o=u.length)){var o,a,l,c=-1;for(r=t.value?r/t.value:0;++cs?-1:1),g=ao.sum(c),v=g?(s-l*p)/g:0,d=ao.range(l),y=[];return null!=e&&d.sort(e===bl?function(n,t){return c[t]-c[n]}:function(n,t){return e(o[n],o[t])}),d.forEach(function(n){y[n]={data:o[n],value:a=c[n],startAngle:f,endAngle:f+=a*v+p,padAngle:h}}),y}var t=Number,e=bl,r=0,i=Ho,u=0;return n.value=function(e){return arguments.length?(t=e,n):t},n.sort=function(t){return arguments.length?(e=t,n):e},n.startAngle=function(t){return arguments.length?(r=t,n):r},n.endAngle=function(t){return arguments.length?(i=t,n):i},n.padAngle=function(t){return arguments.length?(u=t,n):u},n};var bl={};ao.layout.stack=function(){function n(a,l){if(!(h=a.length))return a;var c=a.map(function(e,r){return t.call(n,e,r)}),f=c.map(function(t){return t.map(function(t,e){return[u.call(n,t,e),o.call(n,t,e)]})}),s=e.call(n,f,l);c=ao.permute(c,s),f=ao.permute(f,s);var h,p,g,v,d=r.call(n,f,l),y=c[0].length;for(g=0;y>g;++g)for(i.call(n,c[0][g],v=d[g],f[0][g][1]),p=1;h>p;++p)i.call(n,c[p][g],v+=f[p-1][g][1],f[p][g][1]);return a}var t=m,e=gi,r=vi,i=pi,u=si,o=hi;return n.values=function(e){return arguments.length?(t=e,n):t},n.order=function(t){return arguments.length?(e="function"==typeof t?t:_l.get(t)||gi,n):e},n.offset=function(t){return arguments.length?(r="function"==typeof t?t:wl.get(t)||vi,n):r},n.x=function(t){return arguments.length?(u=t,n):u},n.y=function(t){return arguments.length?(o=t,n):o},n.out=function(t){return arguments.length?(i=t,n):i},n};var _l=ao.map({"inside-out":function(n){var t,e,r=n.length,i=n.map(di),u=n.map(yi),o=ao.range(r).sort(function(n,t){return i[n]-i[t]}),a=0,l=0,c=[],f=[];for(t=0;r>t;++t)e=o[t],l>a?(a+=u[e],c.push(e)):(l+=u[e],f.push(e));return f.reverse().concat(c)},reverse:function(n){return ao.range(n.length).reverse()},"default":gi}),wl=ao.map({silhouette:function(n){var t,e,r,i=n.length,u=n[0].length,o=[],a=0,l=[];for(e=0;u>e;++e){for(t=0,r=0;i>t;t++)r+=n[t][e][1];r>a&&(a=r),o.push(r)}for(e=0;u>e;++e)l[e]=(a-o[e])/2;return l},wiggle:function(n){var t,e,r,i,u,o,a,l,c,f=n.length,s=n[0],h=s.length,p=[];for(p[0]=l=c=0,e=1;h>e;++e){for(t=0,i=0;f>t;++t)i+=n[t][e][1];for(t=0,u=0,a=s[e][0]-s[e-1][0];f>t;++t){for(r=0,o=(n[t][e][1]-n[t][e-1][1])/(2*a);t>r;++r)o+=(n[r][e][1]-n[r][e-1][1])/a;u+=o*n[t][e][1]}p[e]=l-=i?u/i*a:0,c>l&&(c=l)}for(e=0;h>e;++e)p[e]-=c;return p},expand:function(n){var t,e,r,i=n.length,u=n[0].length,o=1/i,a=[];for(e=0;u>e;++e){for(t=0,r=0;i>t;t++)r+=n[t][e][1];if(r)for(t=0;i>t;t++)n[t][e][1]/=r;else for(t=0;i>t;t++)n[t][e][1]=o}for(e=0;u>e;++e)a[e]=0;return a},zero:vi});ao.layout.histogram=function(){function n(n,u){for(var o,a,l=[],c=n.map(e,this),f=r.call(this,c,u),s=i.call(this,f,c,u),u=-1,h=c.length,p=s.length-1,g=t?1:1/h;++u0)for(u=-1;++u=f[0]&&a<=f[1]&&(o=l[ao.bisect(s,a,1,p)-1],o.y+=g,o.push(n[u]));return l}var t=!0,e=Number,r=bi,i=Mi;return n.value=function(t){return arguments.length?(e=t,n):e},n.range=function(t){return arguments.length?(r=En(t),n):r},n.bins=function(t){return arguments.length?(i="number"==typeof t?function(n){return xi(n,t)}:En(t),n):i},n.frequency=function(e){return arguments.length?(t=!!e,n):t},n},ao.layout.pack=function(){function n(n,u){var o=e.call(this,n,u),a=o[0],l=i[0],c=i[1],f=null==t?Math.sqrt:"function"==typeof t?t:function(){return t};if(a.x=a.y=0,oi(a,function(n){n.r=+f(n.value)}),oi(a,Ni),r){var s=r*(t?1:Math.max(2*a.r/l,2*a.r/c))/2;oi(a,function(n){n.r+=s}),oi(a,Ni),oi(a,function(n){n.r-=s})}return Ci(a,l/2,c/2,t?1:1/Math.max(2*a.r/l,2*a.r/c)),o}var t,e=ao.layout.hierarchy().sort(_i),r=0,i=[1,1];return n.size=function(t){return arguments.length?(i=t,n):i},n.radius=function(e){return arguments.length?(t=null==e||"function"==typeof e?e:+e,n):t},n.padding=function(t){return arguments.length?(r=+t,n):r},ii(n,e)},ao.layout.tree=function(){function n(n,i){var f=o.call(this,n,i),s=f[0],h=t(s);if(oi(h,e),h.parent.m=-h.z,ui(h,r),c)ui(s,u);else{var p=s,g=s,v=s;ui(s,function(n){n.xg.x&&(g=n),n.depth>v.depth&&(v=n)});var d=a(p,g)/2-p.x,y=l[0]/(g.x+a(g,p)/2+d),m=l[1]/(v.depth||1);ui(s,function(n){n.x=(n.x+d)*y,n.y=n.depth*m})}return f}function t(n){for(var t,e={A:null,children:[n]},r=[e];null!=(t=r.pop());)for(var i,u=t.children,o=0,a=u.length;a>o;++o)r.push((u[o]=i={_:u[o],parent:t,children:(i=u[o].children)&&i.slice()||[],A:null,a:null,z:0,m:0,c:0,s:0,t:null,i:o}).a=i);return e.children[0]}function e(n){var t=n.children,e=n.parent.children,r=n.i?e[n.i-1]:null;if(t.length){Di(n);var u=(t[0].z+t[t.length-1].z)/2;r?(n.z=r.z+a(n._,r._),n.m=n.z-u):n.z=u}else r&&(n.z=r.z+a(n._,r._));n.parent.A=i(n,r,n.parent.A||e[0])}function r(n){n._.x=n.z+n.parent.m,n.m+=n.parent.m}function i(n,t,e){if(t){for(var r,i=n,u=n,o=t,l=i.parent.children[0],c=i.m,f=u.m,s=o.m,h=l.m;o=Ti(o),i=qi(i),o&&i;)l=qi(l),u=Ti(u),u.a=n,r=o.z+s-i.z-c+a(o._,i._),r>0&&(Ri(Pi(o,n,e),n,r),c+=r,f+=r),s+=o.m,c+=i.m,h+=l.m,f+=u.m;o&&!Ti(u)&&(u.t=o,u.m+=s-f),i&&!qi(l)&&(l.t=i,l.m+=c-h,e=n)}return e}function u(n){n.x*=l[0],n.y=n.depth*l[1]}var o=ao.layout.hierarchy().sort(null).value(null),a=Li,l=[1,1],c=null;return n.separation=function(t){return arguments.length?(a=t,n):a},n.size=function(t){return arguments.length?(c=null==(l=t)?u:null,n):c?null:l},n.nodeSize=function(t){return arguments.length?(c=null==(l=t)?null:u,n):c?l:null},ii(n,o)},ao.layout.cluster=function(){function n(n,u){var o,a=t.call(this,n,u),l=a[0],c=0;oi(l,function(n){var t=n.children;t&&t.length?(n.x=ji(t),n.y=Ui(t)):(n.x=o?c+=e(n,o):0,n.y=0,o=n)});var f=Fi(l),s=Hi(l),h=f.x-e(f,s)/2,p=s.x+e(s,f)/2;return oi(l,i?function(n){n.x=(n.x-l.x)*r[0],n.y=(l.y-n.y)*r[1]}:function(n){n.x=(n.x-h)/(p-h)*r[0],n.y=(1-(l.y?n.y/l.y:1))*r[1]}),a}var t=ao.layout.hierarchy().sort(null).value(null),e=Li,r=[1,1],i=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(i=null==(r=t),n):i?null:r},n.nodeSize=function(t){return arguments.length?(i=null!=(r=t),n):i?r:null},ii(n,t)},ao.layout.treemap=function(){function n(n,t){for(var e,r,i=-1,u=n.length;++it?0:t),e.area=isNaN(r)||0>=r?0:r}function t(e){var u=e.children;if(u&&u.length){var o,a,l,c=s(e),f=[],h=u.slice(),g=1/0,v="slice"===p?c.dx:"dice"===p?c.dy:"slice-dice"===p?1&e.depth?c.dy:c.dx:Math.min(c.dx,c.dy);for(n(h,c.dx*c.dy/e.value),f.area=0;(l=h.length)>0;)f.push(o=h[l-1]),f.area+=o.area,"squarify"!==p||(a=r(f,v))<=g?(h.pop(),g=a):(f.area-=f.pop().area,i(f,v,c,!1),v=Math.min(c.dx,c.dy),f.length=f.area=0,g=1/0);f.length&&(i(f,v,c,!0),f.length=f.area=0),u.forEach(t)}}function e(t){var r=t.children;if(r&&r.length){var u,o=s(t),a=r.slice(),l=[];for(n(a,o.dx*o.dy/t.value),l.area=0;u=a.pop();)l.push(u),l.area+=u.area,null!=u.z&&(i(l,u.z?o.dx:o.dy,o,!a.length),l.length=l.area=0);r.forEach(e)}}function r(n,t){for(var e,r=n.area,i=0,u=1/0,o=-1,a=n.length;++oe&&(u=e),e>i&&(i=e));return r*=r,t*=t,r?Math.max(t*i*g/r,r/(t*u*g)):1/0}function i(n,t,e,r){var i,u=-1,o=n.length,a=e.x,c=e.y,f=t?l(n.area/t):0; +if(t==e.dx){for((r||f>e.dy)&&(f=e.dy);++ue.dx)&&(f=e.dx);++ue&&(t=1),1>e&&(n=0),function(){var e,r,i;do e=2*Math.random()-1,r=2*Math.random()-1,i=e*e+r*r;while(!i||i>1);return n+t*e*Math.sqrt(-2*Math.log(i)/i)}},logNormal:function(){var n=ao.random.normal.apply(ao,arguments);return function(){return Math.exp(n())}},bates:function(n){var t=ao.random.irwinHall(n);return function(){return t()/n}},irwinHall:function(n){return function(){for(var t=0,e=0;n>e;e++)t+=Math.random();return t}}},ao.scale={};var Sl={floor:m,ceil:m};ao.scale.linear=function(){return Wi([0,1],[0,1],Mr,!1)};var kl={s:1,g:1,p:1,r:1,e:1};ao.scale.log=function(){return ru(ao.scale.linear().domain([0,1]),10,!0,[1,10])};var Nl=ao.format(".0e"),El={floor:function(n){return-Math.ceil(-n)},ceil:function(n){return-Math.floor(-n)}};ao.scale.pow=function(){return iu(ao.scale.linear(),1,[0,1])},ao.scale.sqrt=function(){return ao.scale.pow().exponent(.5)},ao.scale.ordinal=function(){return ou([],{t:"range",a:[[]]})},ao.scale.category10=function(){return ao.scale.ordinal().range(Al)},ao.scale.category20=function(){return ao.scale.ordinal().range(Cl)},ao.scale.category20b=function(){return ao.scale.ordinal().range(zl)},ao.scale.category20c=function(){return ao.scale.ordinal().range(Ll)};var Al=[2062260,16744206,2924588,14034728,9725885,9197131,14907330,8355711,12369186,1556175].map(xn),Cl=[2062260,11454440,16744206,16759672,2924588,10018698,14034728,16750742,9725885,12955861,9197131,12885140,14907330,16234194,8355711,13092807,12369186,14408589,1556175,10410725].map(xn),zl=[3750777,5395619,7040719,10264286,6519097,9216594,11915115,13556636,9202993,12426809,15186514,15190932,8666169,11356490,14049643,15177372,8077683,10834324,13528509,14589654].map(xn),Ll=[3244733,7057110,10406625,13032431,15095053,16616764,16625259,16634018,3253076,7652470,10607003,13101504,7695281,10394312,12369372,14342891,6513507,9868950,12434877,14277081].map(xn);ao.scale.quantile=function(){return au([],[])},ao.scale.quantize=function(){return lu(0,1,[0,1])},ao.scale.threshold=function(){return cu([.5],[0,1])},ao.scale.identity=function(){return fu([0,1])},ao.svg={},ao.svg.arc=function(){function n(){var n=Math.max(0,+e.apply(this,arguments)),c=Math.max(0,+r.apply(this,arguments)),f=o.apply(this,arguments)-Io,s=a.apply(this,arguments)-Io,h=Math.abs(s-f),p=f>s?0:1;if(n>c&&(g=c,c=n,n=g),h>=Oo)return t(c,p)+(n?t(n,1-p):"")+"Z";var g,v,d,y,m,M,x,b,_,w,S,k,N=0,E=0,A=[];if((y=(+l.apply(this,arguments)||0)/2)&&(d=u===ql?Math.sqrt(n*n+c*c):+u.apply(this,arguments),p||(E*=-1),c&&(E=tn(d/c*Math.sin(y))),n&&(N=tn(d/n*Math.sin(y)))),c){m=c*Math.cos(f+E),M=c*Math.sin(f+E),x=c*Math.cos(s-E),b=c*Math.sin(s-E);var C=Math.abs(s-f-2*E)<=Fo?0:1;if(E&&yu(m,M,x,b)===p^C){var z=(f+s)/2;m=c*Math.cos(z),M=c*Math.sin(z),x=b=null}}else m=M=0;if(n){_=n*Math.cos(s-N),w=n*Math.sin(s-N),S=n*Math.cos(f+N),k=n*Math.sin(f+N);var L=Math.abs(f-s+2*N)<=Fo?0:1;if(N&&yu(_,w,S,k)===1-p^L){var q=(f+s)/2;_=n*Math.cos(q),w=n*Math.sin(q),S=k=null}}else _=w=0;if(h>Uo&&(g=Math.min(Math.abs(c-n)/2,+i.apply(this,arguments)))>.001){v=c>n^p?0:1;var T=g,R=g;if(Fo>h){var D=null==S?[_,w]:null==x?[m,M]:Re([m,M],[S,k],[x,b],[_,w]),P=m-D[0],U=M-D[1],j=x-D[0],F=b-D[1],H=1/Math.sin(Math.acos((P*j+U*F)/(Math.sqrt(P*P+U*U)*Math.sqrt(j*j+F*F)))/2),O=Math.sqrt(D[0]*D[0]+D[1]*D[1]);R=Math.min(g,(n-O)/(H-1)),T=Math.min(g,(c-O)/(H+1))}if(null!=x){var I=mu(null==S?[_,w]:[S,k],[m,M],c,T,p),Y=mu([x,b],[_,w],c,T,p);g===T?A.push("M",I[0],"A",T,",",T," 0 0,",v," ",I[1],"A",c,",",c," 0 ",1-p^yu(I[1][0],I[1][1],Y[1][0],Y[1][1]),",",p," ",Y[1],"A",T,",",T," 0 0,",v," ",Y[0]):A.push("M",I[0],"A",T,",",T," 0 1,",v," ",Y[0])}else A.push("M",m,",",M);if(null!=S){var Z=mu([m,M],[S,k],n,-R,p),V=mu([_,w],null==x?[m,M]:[x,b],n,-R,p);g===R?A.push("L",V[0],"A",R,",",R," 0 0,",v," ",V[1],"A",n,",",n," 0 ",p^yu(V[1][0],V[1][1],Z[1][0],Z[1][1]),",",1-p," ",Z[1],"A",R,",",R," 0 0,",v," ",Z[0]):A.push("L",V[0],"A",R,",",R," 0 0,",v," ",Z[0])}else A.push("L",_,",",w)}else A.push("M",m,",",M),null!=x&&A.push("A",c,",",c," 0 ",C,",",p," ",x,",",b),A.push("L",_,",",w),null!=S&&A.push("A",n,",",n," 0 ",L,",",1-p," ",S,",",k);return A.push("Z"),A.join("")}function t(n,t){return"M0,"+n+"A"+n+","+n+" 0 1,"+t+" 0,"+-n+"A"+n+","+n+" 0 1,"+t+" 0,"+n}var e=hu,r=pu,i=su,u=ql,o=gu,a=vu,l=du;return n.innerRadius=function(t){return arguments.length?(e=En(t),n):e},n.outerRadius=function(t){return arguments.length?(r=En(t),n):r},n.cornerRadius=function(t){return arguments.length?(i=En(t),n):i},n.padRadius=function(t){return arguments.length?(u=t==ql?ql:En(t),n):u},n.startAngle=function(t){return arguments.length?(o=En(t),n):o},n.endAngle=function(t){return arguments.length?(a=En(t),n):a},n.padAngle=function(t){return arguments.length?(l=En(t),n):l},n.centroid=function(){var n=(+e.apply(this,arguments)+ +r.apply(this,arguments))/2,t=(+o.apply(this,arguments)+ +a.apply(this,arguments))/2-Io;return[Math.cos(t)*n,Math.sin(t)*n]},n};var ql="auto";ao.svg.line=function(){return Mu(m)};var Tl=ao.map({linear:xu,"linear-closed":bu,step:_u,"step-before":wu,"step-after":Su,basis:zu,"basis-open":Lu,"basis-closed":qu,bundle:Tu,cardinal:Eu,"cardinal-open":ku,"cardinal-closed":Nu,monotone:Fu});Tl.forEach(function(n,t){t.key=n,t.closed=/-closed$/.test(n)});var Rl=[0,2/3,1/3,0],Dl=[0,1/3,2/3,0],Pl=[0,1/6,2/3,1/6];ao.svg.line.radial=function(){var n=Mu(Hu);return n.radius=n.x,delete n.x,n.angle=n.y,delete n.y,n},wu.reverse=Su,Su.reverse=wu,ao.svg.area=function(){return Ou(m)},ao.svg.area.radial=function(){var n=Ou(Hu);return n.radius=n.x,delete n.x,n.innerRadius=n.x0,delete n.x0,n.outerRadius=n.x1,delete n.x1,n.angle=n.y,delete n.y,n.startAngle=n.y0,delete n.y0,n.endAngle=n.y1,delete n.y1,n},ao.svg.chord=function(){function n(n,a){var l=t(this,u,n,a),c=t(this,o,n,a);return"M"+l.p0+r(l.r,l.p1,l.a1-l.a0)+(e(l,c)?i(l.r,l.p1,l.r,l.p0):i(l.r,l.p1,c.r,c.p0)+r(c.r,c.p1,c.a1-c.a0)+i(c.r,c.p1,l.r,l.p0))+"Z"}function t(n,t,e,r){var i=t.call(n,e,r),u=a.call(n,i,r),o=l.call(n,i,r)-Io,f=c.call(n,i,r)-Io;return{r:u,a0:o,a1:f,p0:[u*Math.cos(o),u*Math.sin(o)],p1:[u*Math.cos(f),u*Math.sin(f)]}}function e(n,t){return n.a0==t.a0&&n.a1==t.a1}function r(n,t,e){return"A"+n+","+n+" 0 "+ +(e>Fo)+",1 "+t}function i(n,t,e,r){return"Q 0,0 "+r}var u=Me,o=xe,a=Iu,l=gu,c=vu;return n.radius=function(t){return arguments.length?(a=En(t),n):a},n.source=function(t){return arguments.length?(u=En(t),n):u},n.target=function(t){return arguments.length?(o=En(t),n):o},n.startAngle=function(t){return arguments.length?(l=En(t),n):l},n.endAngle=function(t){return arguments.length?(c=En(t),n):c},n},ao.svg.diagonal=function(){function n(n,i){var u=t.call(this,n,i),o=e.call(this,n,i),a=(u.y+o.y)/2,l=[u,{x:u.x,y:a},{x:o.x,y:a},o];return l=l.map(r),"M"+l[0]+"C"+l[1]+" "+l[2]+" "+l[3]}var t=Me,e=xe,r=Yu;return n.source=function(e){return arguments.length?(t=En(e),n):t},n.target=function(t){return arguments.length?(e=En(t),n):e},n.projection=function(t){return arguments.length?(r=t,n):r},n},ao.svg.diagonal.radial=function(){var n=ao.svg.diagonal(),t=Yu,e=n.projection;return n.projection=function(n){return arguments.length?e(Zu(t=n)):t},n},ao.svg.symbol=function(){function n(n,r){return(Ul.get(t.call(this,n,r))||$u)(e.call(this,n,r))}var t=Xu,e=Vu;return n.type=function(e){return arguments.length?(t=En(e),n):t},n.size=function(t){return arguments.length?(e=En(t),n):e},n};var Ul=ao.map({circle:$u,cross:function(n){var t=Math.sqrt(n/5)/2;return"M"+-3*t+","+-t+"H"+-t+"V"+-3*t+"H"+t+"V"+-t+"H"+3*t+"V"+t+"H"+t+"V"+3*t+"H"+-t+"V"+t+"H"+-3*t+"Z"},diamond:function(n){var t=Math.sqrt(n/(2*Fl)),e=t*Fl;return"M0,"+-t+"L"+e+",0 0,"+t+" "+-e+",0Z"},square:function(n){var t=Math.sqrt(n)/2;return"M"+-t+","+-t+"L"+t+","+-t+" "+t+","+t+" "+-t+","+t+"Z"},"triangle-down":function(n){var t=Math.sqrt(n/jl),e=t*jl/2;return"M0,"+e+"L"+t+","+-e+" "+-t+","+-e+"Z"},"triangle-up":function(n){var t=Math.sqrt(n/jl),e=t*jl/2;return"M0,"+-e+"L"+t+","+e+" "+-t+","+e+"Z"}});ao.svg.symbolTypes=Ul.keys();var jl=Math.sqrt(3),Fl=Math.tan(30*Yo);Co.transition=function(n){for(var t,e,r=Hl||++Zl,i=Ku(n),u=[],o=Ol||{time:Date.now(),ease:Nr,delay:0,duration:250},a=-1,l=this.length;++au;u++){i.push(t=[]);for(var e=this[u],a=0,l=e.length;l>a;a++)(r=e[a])&&n.call(r,r.__data__,a,u)&&t.push(r)}return Wu(i,this.namespace,this.id)},Yl.tween=function(n,t){var e=this.id,r=this.namespace;return arguments.length<2?this.node()[r][e].tween.get(n):Y(this,null==t?function(t){t[r][e].tween.remove(n)}:function(i){i[r][e].tween.set(n,t)})},Yl.attr=function(n,t){function e(){this.removeAttribute(a)}function r(){this.removeAttributeNS(a.space,a.local)}function i(n){return null==n?e:(n+="",function(){var t,e=this.getAttribute(a);return e!==n&&(t=o(e,n),function(n){this.setAttribute(a,t(n))})})}function u(n){return null==n?r:(n+="",function(){var t,e=this.getAttributeNS(a.space,a.local);return e!==n&&(t=o(e,n),function(n){this.setAttributeNS(a.space,a.local,t(n))})})}if(arguments.length<2){for(t in n)this.attr(t,n[t]);return this}var o="transform"==n?$r:Mr,a=ao.ns.qualify(n);return Ju(this,"attr."+n,t,a.local?u:i)},Yl.attrTween=function(n,t){function e(n,e){var r=t.call(this,n,e,this.getAttribute(i));return r&&function(n){this.setAttribute(i,r(n))}}function r(n,e){var r=t.call(this,n,e,this.getAttributeNS(i.space,i.local));return r&&function(n){this.setAttributeNS(i.space,i.local,r(n))}}var i=ao.ns.qualify(n);return this.tween("attr."+n,i.local?r:e)},Yl.style=function(n,e,r){function i(){this.style.removeProperty(n)}function u(e){return null==e?i:(e+="",function(){var i,u=t(this).getComputedStyle(this,null).getPropertyValue(n);return u!==e&&(i=Mr(u,e),function(t){this.style.setProperty(n,i(t),r)})})}var o=arguments.length;if(3>o){if("string"!=typeof n){2>o&&(e="");for(r in n)this.style(r,n[r],e);return this}r=""}return Ju(this,"style."+n,e,u)},Yl.styleTween=function(n,e,r){function i(i,u){var o=e.call(this,i,u,t(this).getComputedStyle(this,null).getPropertyValue(n));return o&&function(t){this.style.setProperty(n,o(t),r)}}return arguments.length<3&&(r=""),this.tween("style."+n,i)},Yl.text=function(n){return Ju(this,"text",n,Gu)},Yl.remove=function(){var n=this.namespace;return this.each("end.transition",function(){var t;this[n].count<2&&(t=this.parentNode)&&t.removeChild(this)})},Yl.ease=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].ease:("function"!=typeof n&&(n=ao.ease.apply(ao,arguments)),Y(this,function(r){r[e][t].ease=n}))},Yl.delay=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].delay:Y(this,"function"==typeof n?function(r,i,u){r[e][t].delay=+n.call(r,r.__data__,i,u)}:(n=+n,function(r){r[e][t].delay=n}))},Yl.duration=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].duration:Y(this,"function"==typeof n?function(r,i,u){r[e][t].duration=Math.max(1,n.call(r,r.__data__,i,u))}:(n=Math.max(1,n),function(r){r[e][t].duration=n}))},Yl.each=function(n,t){var e=this.id,r=this.namespace;if(arguments.length<2){var i=Ol,u=Hl;try{Hl=e,Y(this,function(t,i,u){Ol=t[r][e],n.call(t,t.__data__,i,u)})}finally{Ol=i,Hl=u}}else Y(this,function(i){var u=i[r][e];(u.event||(u.event=ao.dispatch("start","end","interrupt"))).on(n,t)});return this},Yl.transition=function(){for(var n,t,e,r,i=this.id,u=++Zl,o=this.namespace,a=[],l=0,c=this.length;c>l;l++){a.push(n=[]);for(var t=this[l],f=0,s=t.length;s>f;f++)(e=t[f])&&(r=e[o][i],Qu(e,f,o,u,{time:r.time,ease:r.ease,delay:r.delay+r.duration,duration:r.duration})),n.push(e)}return Wu(a,o,u)},ao.svg.axis=function(){function n(n){n.each(function(){var n,c=ao.select(this),f=this.__chart__||e,s=this.__chart__=e.copy(),h=null==l?s.ticks?s.ticks.apply(s,a):s.domain():l,p=null==t?s.tickFormat?s.tickFormat.apply(s,a):m:t,g=c.selectAll(".tick").data(h,s),v=g.enter().insert("g",".domain").attr("class","tick").style("opacity",Uo),d=ao.transition(g.exit()).style("opacity",Uo).remove(),y=ao.transition(g.order()).style("opacity",1),M=Math.max(i,0)+o,x=Zi(s),b=c.selectAll(".domain").data([0]),_=(b.enter().append("path").attr("class","domain"),ao.transition(b));v.append("line"),v.append("text");var w,S,k,N,E=v.select("line"),A=y.select("line"),C=g.select("text").text(p),z=v.select("text"),L=y.select("text"),q="top"===r||"left"===r?-1:1;if("bottom"===r||"top"===r?(n=no,w="x",k="y",S="x2",N="y2",C.attr("dy",0>q?"0em":".71em").style("text-anchor","middle"),_.attr("d","M"+x[0]+","+q*u+"V0H"+x[1]+"V"+q*u)):(n=to,w="y",k="x",S="y2",N="x2",C.attr("dy",".32em").style("text-anchor",0>q?"end":"start"),_.attr("d","M"+q*u+","+x[0]+"H0V"+x[1]+"H"+q*u)),E.attr(N,q*i),z.attr(k,q*M),A.attr(S,0).attr(N,q*i),L.attr(w,0).attr(k,q*M),s.rangeBand){var T=s,R=T.rangeBand()/2;f=s=function(n){return T(n)+R}}else f.rangeBand?f=s:d.call(n,s,f);v.call(n,f,s),y.call(n,s,s)})}var t,e=ao.scale.linear(),r=Vl,i=6,u=6,o=3,a=[10],l=null;return n.scale=function(t){return arguments.length?(e=t,n):e},n.orient=function(t){return arguments.length?(r=t in Xl?t+"":Vl,n):r},n.ticks=function(){return arguments.length?(a=co(arguments),n):a},n.tickValues=function(t){return arguments.length?(l=t,n):l},n.tickFormat=function(e){return arguments.length?(t=e,n):t},n.tickSize=function(t){var e=arguments.length;return e?(i=+t,u=+arguments[e-1],n):i},n.innerTickSize=function(t){return arguments.length?(i=+t,n):i},n.outerTickSize=function(t){return arguments.length?(u=+t,n):u},n.tickPadding=function(t){return arguments.length?(o=+t,n):o},n.tickSubdivide=function(){return arguments.length&&n},n};var Vl="bottom",Xl={top:1,right:1,bottom:1,left:1};ao.svg.brush=function(){function n(t){t.each(function(){var t=ao.select(this).style("pointer-events","all").style("-webkit-tap-highlight-color","rgba(0,0,0,0)").on("mousedown.brush",u).on("touchstart.brush",u),o=t.selectAll(".background").data([0]);o.enter().append("rect").attr("class","background").style("visibility","hidden").style("cursor","crosshair"),t.selectAll(".extent").data([0]).enter().append("rect").attr("class","extent").style("cursor","move");var a=t.selectAll(".resize").data(v,m);a.exit().remove(),a.enter().append("g").attr("class",function(n){return"resize "+n}).style("cursor",function(n){return $l[n]}).append("rect").attr("x",function(n){return/[ew]$/.test(n)?-3:null}).attr("y",function(n){return/^[ns]/.test(n)?-3:null}).attr("width",6).attr("height",6).style("visibility","hidden"),a.style("display",n.empty()?"none":null);var l,s=ao.transition(t),h=ao.transition(o);c&&(l=Zi(c),h.attr("x",l[0]).attr("width",l[1]-l[0]),r(s)),f&&(l=Zi(f),h.attr("y",l[0]).attr("height",l[1]-l[0]),i(s)),e(s)})}function e(n){n.selectAll(".resize").attr("transform",function(n){return"translate("+s[+/e$/.test(n)]+","+h[+/^s/.test(n)]+")"})}function r(n){n.select(".extent").attr("x",s[0]),n.selectAll(".extent,.n>rect,.s>rect").attr("width",s[1]-s[0])}function i(n){n.select(".extent").attr("y",h[0]),n.selectAll(".extent,.e>rect,.w>rect").attr("height",h[1]-h[0])}function u(){function u(){32==ao.event.keyCode&&(C||(M=null,L[0]-=s[1],L[1]-=h[1],C=2),S())}function v(){32==ao.event.keyCode&&2==C&&(L[0]+=s[1],L[1]+=h[1],C=0,S())}function d(){var n=ao.mouse(b),t=!1;x&&(n[0]+=x[0],n[1]+=x[1]),C||(ao.event.altKey?(M||(M=[(s[0]+s[1])/2,(h[0]+h[1])/2]),L[0]=s[+(n[0]f?(i=r,r=f):i=f),v[0]!=r||v[1]!=i?(e?a=null:o=null,v[0]=r,v[1]=i,!0):void 0}function m(){d(),k.style("pointer-events","all").selectAll(".resize").style("display",n.empty()?"none":null),ao.select("body").style("cursor",null),q.on("mousemove.brush",null).on("mouseup.brush",null).on("touchmove.brush",null).on("touchend.brush",null).on("keydown.brush",null).on("keyup.brush",null),z(),w({type:"brushend"})}var M,x,b=this,_=ao.select(ao.event.target),w=l.of(b,arguments),k=ao.select(b),N=_.datum(),E=!/^(n|s)$/.test(N)&&c,A=!/^(e|w)$/.test(N)&&f,C=_.classed("extent"),z=W(b),L=ao.mouse(b),q=ao.select(t(b)).on("keydown.brush",u).on("keyup.brush",v);if(ao.event.changedTouches?q.on("touchmove.brush",d).on("touchend.brush",m):q.on("mousemove.brush",d).on("mouseup.brush",m),k.interrupt().selectAll("*").interrupt(),C)L[0]=s[0]-L[0],L[1]=h[0]-L[1];else if(N){var T=+/w$/.test(N),R=+/^n/.test(N);x=[s[1-T]-L[0],h[1-R]-L[1]],L[0]=s[T],L[1]=h[R]}else ao.event.altKey&&(M=L.slice());k.style("pointer-events","none").selectAll(".resize").style("display",null),ao.select("body").style("cursor",_.style("cursor")),w({type:"brushstart"}),d()}var o,a,l=N(n,"brushstart","brush","brushend"),c=null,f=null,s=[0,0],h=[0,0],p=!0,g=!0,v=Bl[0];return n.event=function(n){n.each(function(){var n=l.of(this,arguments),t={x:s,y:h,i:o,j:a},e=this.__chart__||t;this.__chart__=t,Hl?ao.select(this).transition().each("start.brush",function(){o=e.i,a=e.j,s=e.x,h=e.y,n({type:"brushstart"})}).tween("brush:brush",function(){var e=xr(s,t.x),r=xr(h,t.y);return o=a=null,function(i){s=t.x=e(i),h=t.y=r(i),n({type:"brush",mode:"resize"})}}).each("end.brush",function(){o=t.i,a=t.j,n({type:"brush",mode:"resize"}),n({type:"brushend"})}):(n({type:"brushstart"}),n({type:"brush",mode:"resize"}),n({type:"brushend"}))})},n.x=function(t){return arguments.length?(c=t,v=Bl[!c<<1|!f],n):c},n.y=function(t){return arguments.length?(f=t,v=Bl[!c<<1|!f],n):f},n.clamp=function(t){return arguments.length?(c&&f?(p=!!t[0],g=!!t[1]):c?p=!!t:f&&(g=!!t),n):c&&f?[p,g]:c?p:f?g:null},n.extent=function(t){var e,r,i,u,l;return arguments.length?(c&&(e=t[0],r=t[1],f&&(e=e[0],r=r[0]),o=[e,r],c.invert&&(e=c(e),r=c(r)),e>r&&(l=e,e=r,r=l),e==s[0]&&r==s[1]||(s=[e,r])),f&&(i=t[0],u=t[1],c&&(i=i[1],u=u[1]),a=[i,u],f.invert&&(i=f(i),u=f(u)),i>u&&(l=i,i=u,u=l),i==h[0]&&u==h[1]||(h=[i,u])),n):(c&&(o?(e=o[0],r=o[1]):(e=s[0],r=s[1],c.invert&&(e=c.invert(e),r=c.invert(r)),e>r&&(l=e,e=r,r=l))),f&&(a?(i=a[0],u=a[1]):(i=h[0],u=h[1],f.invert&&(i=f.invert(i),u=f.invert(u)),i>u&&(l=i,i=u,u=l))),c&&f?[[e,i],[r,u]]:c?[e,r]:f&&[i,u])},n.clear=function(){return n.empty()||(s=[0,0],h=[0,0],o=a=null),n},n.empty=function(){return!!c&&s[0]==s[1]||!!f&&h[0]==h[1]},ao.rebind(n,l,"on")};var $l={n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},Bl=[["n","e","s","w","nw","ne","se","sw"],["e","w"],["n","s"],[]],Wl=ga.format=xa.timeFormat,Jl=Wl.utc,Gl=Jl("%Y-%m-%dT%H:%M:%S.%LZ");Wl.iso=Date.prototype.toISOString&&+new Date("2000-01-01T00:00:00.000Z")?eo:Gl,eo.parse=function(n){var t=new Date(n);return isNaN(t)?null:t},eo.toString=Gl.toString,ga.second=On(function(n){return new va(1e3*Math.floor(n/1e3))},function(n,t){n.setTime(n.getTime()+1e3*Math.floor(t))},function(n){return n.getSeconds()}),ga.seconds=ga.second.range,ga.seconds.utc=ga.second.utc.range,ga.minute=On(function(n){return new va(6e4*Math.floor(n/6e4))},function(n,t){n.setTime(n.getTime()+6e4*Math.floor(t))},function(n){return n.getMinutes()}),ga.minutes=ga.minute.range,ga.minutes.utc=ga.minute.utc.range,ga.hour=On(function(n){var t=n.getTimezoneOffset()/60;return new va(36e5*(Math.floor(n/36e5-t)+t))},function(n,t){n.setTime(n.getTime()+36e5*Math.floor(t))},function(n){return n.getHours()}),ga.hours=ga.hour.range,ga.hours.utc=ga.hour.utc.range,ga.month=On(function(n){return n=ga.day(n),n.setDate(1),n},function(n,t){n.setMonth(n.getMonth()+t)},function(n){return n.getMonth()}),ga.months=ga.month.range,ga.months.utc=ga.month.utc.range;var Kl=[1e3,5e3,15e3,3e4,6e4,3e5,9e5,18e5,36e5,108e5,216e5,432e5,864e5,1728e5,6048e5,2592e6,7776e6,31536e6],Ql=[[ga.second,1],[ga.second,5],[ga.second,15],[ga.second,30],[ga.minute,1],[ga.minute,5],[ga.minute,15],[ga.minute,30],[ga.hour,1],[ga.hour,3],[ga.hour,6],[ga.hour,12],[ga.day,1],[ga.day,2],[ga.week,1],[ga.month,1],[ga.month,3],[ga.year,1]],nc=Wl.multi([[".%L",function(n){return n.getMilliseconds()}],[":%S",function(n){return n.getSeconds()}],["%I:%M",function(n){return n.getMinutes()}],["%I %p",function(n){return n.getHours()}],["%a %d",function(n){return n.getDay()&&1!=n.getDate()}],["%b %d",function(n){return 1!=n.getDate()}],["%B",function(n){return n.getMonth()}],["%Y",zt]]),tc={range:function(n,t,e){return ao.range(Math.ceil(n/e)*e,+t,e).map(io)},floor:m,ceil:m};Ql.year=ga.year,ga.scale=function(){return ro(ao.scale.linear(),Ql,nc)};var ec=Ql.map(function(n){return[n[0].utc,n[1]]}),rc=Jl.multi([[".%L",function(n){return n.getUTCMilliseconds()}],[":%S",function(n){return n.getUTCSeconds()}],["%I:%M",function(n){return n.getUTCMinutes()}],["%I %p",function(n){return n.getUTCHours()}],["%a %d",function(n){return n.getUTCDay()&&1!=n.getUTCDate()}],["%b %d",function(n){return 1!=n.getUTCDate()}],["%B",function(n){return n.getUTCMonth()}],["%Y",zt]]);ec.year=ga.year.utc,ga.scale.utc=function(){return ro(ao.scale.linear(),ec,rc)},ao.text=An(function(n){return n.responseText}),ao.json=function(n,t){return Cn(n,"application/json",uo,t)},ao.html=function(n,t){return Cn(n,"text/html",oo,t)},ao.xml=An(function(n){return n.responseXML}),"function"==typeof define&&define.amd?(this.d3=ao,define(ao)):"object"==typeof module&&module.exports?module.exports=ao:this.d3=ao}(); \ No newline at end of file diff --git a/public/map_js/datamaps.world.hires.min.js b/public/map_js/datamaps.world.hires.min.js new file mode 100644 index 0000000..c26389e --- /dev/null +++ b/public/map_js/datamaps.world.hires.min.js @@ -0,0 +1,5 @@ +!function(){function a(a,b,c){"undefined"==typeof c&&(c=b,optionsValues=void 0);var d="undefined"!=typeof a?a:b;if("undefined"==typeof d)return null;if("function"==typeof d){var e=[c];return c.geography&&(e=[c.geography,c.data]),d.apply(null,e)}return d}function b(a,b,c){return this.svg=n.select(a).append("svg").attr("width",c||a.offsetWidth).attr("data-width",c||a.offsetWidth).attr("class","datamap").attr("height",b||a.offsetHeight).style("overflow","hidden"),this.options.responsive&&(n.select(this.options.element).style({position:"relative","padding-bottom":100*this.options.aspectRatio+"%"}),n.select(this.options.element).select("svg").style({position:"absolute",width:"100%",height:"100%"}),n.select(this.options.element).select("svg").select("g").selectAll("path").style("vector-effect","non-scaling-stroke")),this.svg}function c(a,b){var c,d,e=b.width||a.offsetWidth,f=b.height||a.offsetHeight,g=this.svg;return b&&"undefined"==typeof b.scope&&(b.scope="world"),"usa"===b.scope?c=n.geo.albersUsa().scale(e).translate([e/2,f/2]):"world"===b.scope&&(c=n.geo[b.projection]().scale((e+1)/2/Math.PI).translate([e/2,f/("mercator"===b.projection?1.45:1.8)])),"orthographic"===b.projection&&(g.append("defs").append("path").datum({type:"Sphere"}).attr("id","sphere").attr("d",d),g.append("use").attr("class","stroke").attr("xlink:href","#sphere"),g.append("use").attr("class","fill").attr("xlink:href","#sphere"),c.scale(250).clipAngle(90).rotate(b.projectionConfig.rotation)),d=n.geo.path().projection(c),{path:d,projection:c}}function d(){n.select(".datamaps-style-block").empty()&&n.select("head").append("style").attr("class","datamaps-style-block").html('.datamap path.datamaps-graticule { fill: none; stroke: #777; stroke-width: 0.5px; stroke-opacity: .5; pointer-events: none; } .datamap .labels {pointer-events: none;} .datamap path:not(.datamaps-arc), .datamap circle, .datamap line {stroke: #FFFFFF; vector-effect: non-scaling-stroke; stroke-width: 1px;} .datamaps-legend dt, .datamaps-legend dd { float: left; margin: 0 3px 0 0;} .datamaps-legend dd {width: 20px; margin-right: 6px; border-radius: 3px;} .datamaps-legend {padding-bottom: 20px; z-index: 1001; position: absolute; left: 4px; font-size: 12px; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;} .datamaps-hoverover {display: none; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; } .hoverinfo {padding: 4px; border-radius: 1px; background-color: #FFF; box-shadow: 1px 1px 5px #CCC; font-size: 12px; border: 1px solid #CCC; } .hoverinfo hr {border:1px dotted #CCC; }')}function e(b){var c=this.options.fills,d=this.options.data||{},e=this.options.geographyConfig,f=this.svg.select("g.datamaps-subunits");f.empty()&&(f=this.addLayer("datamaps-subunits",null,!0));var g=o.feature(b,b.objects[this.options.scope]).features;e.hideAntarctica&&(g=g.filter(function(a){return"ATA"!==a.id})),e.hideHawaiiAndAlaska&&(g=g.filter(function(a){return"HI"!==a.id&&"AK"!==a.id}));var h=f.selectAll("path.datamaps-subunit").data(g);h.enter().append("path").attr("d",this.path).attr("class",function(a){return"datamaps-subunit "+a.id}).attr("data-info",function(a){return JSON.stringify(d[a.id])}).style("fill",function(b){var e,f=d[b.id];return f&&f.fillKey&&(e=c[a(f.fillKey,{data:d[b.id],geography:b})]),"undefined"==typeof e&&(e=a(f&&f.fillColor,c.defaultFill,{data:d[b.id],geography:b})),e}).style("stroke-width",e.borderWidth).style("stroke-opacity",e.borderOpacity).style("stroke",e.borderColor)}function f(){function b(){this.parentNode.appendChild(this)}var c=this.svg,d=this,e=this.options.geographyConfig;(e.highlightOnHover||e.popupOnHover)&&c.selectAll(".datamaps-subunit").on("mouseover",function(f){var g=n.select(this),h=d.options.data[f.id]||{};if(e.highlightOnHover){var i={fill:g.style("fill"),stroke:g.style("stroke"),"stroke-width":g.style("stroke-width"),"fill-opacity":g.style("fill-opacity")};g.style("fill",a(h.highlightFillColor,e.highlightFillColor,h)).style("stroke",a(h.highlightBorderColor,e.highlightBorderColor,h)).style("stroke-width",a(h.highlightBorderWidth,e.highlightBorderWidth,h)).style("stroke-opacity",a(h.highlightBorderOpacity,e.highlightBorderOpacity,h)).style("fill-opacity",a(h.highlightFillOpacity,e.highlightFillOpacity,h)).attr("data-previousAttributes",JSON.stringify(i)),/((MSIE)|(Trident))/.test(navigator.userAgent)||b.call(this)}e.popupOnHover&&d.updatePopup(g,f,e,c)}).on("mouseout",function(){var a=n.select(this);if(e.highlightOnHover){var b=JSON.parse(a.attr("data-previousAttributes"));for(var c in b)a.style(c,b[c])}a.on("mousemove",null),n.selectAll(".datamaps-hoverover").style("display","none")})}function g(a,b,c){if(b=b||{},this.options.fills){var d="
",e="";b.legendTitle&&(d="

"+b.legendTitle+"

"+d);for(var f in this.options.fills){if("defaultFill"===f){if(!b.defaultFillName)continue;e=b.defaultFillName}else e=b.labels&&b.labels[f]?b.labels[f]:f+": ";d+="
"+e+"
",d+='
 
'}d+="
";n.select(this.options.element).append("div").attr("class","datamaps-legend").html(d)}}function h(a,b){var c=n.geo.graticule();this.svg.insert("path",".datamaps-subunits").datum(c).attr("class","datamaps-graticule").attr("d",this.path)}function i(b,c,d){var e=this,f=this.svg;if(!c||c&&!c.slice)throw"Datamaps Error - arcs must be an array";for(var g=0;g-1&&(g=-2.5),"NY"===e.id&&(g=-1),"MI"===e.id&&(h=18),"LA"===e.id&&(g=13);var i,j;i=f[0]-g,j=f[1]+h;var k=["VT","NH","MA","RI","CT","NJ","DE","MD","DC"].indexOf(e.id);if(k>-1){var l=d[1];i=d[0],j=l+k*(2+(b.fontSize||12)),a.append("line").attr("x1",i-3).attr("y1",j-5).attr("x2",f[0]).attr("y2",f[1]).style("stroke",b.labelColor||"#000").style("stroke-width",b.lineWidth||1)}return a.append("text").attr("x",i).attr("y",j).style("font-size",(b.fontSize||10)+"px").style("font-family",b.fontFamily||"Verdana").style("fill",b.labelColor||"#000").text(function(){return b.customLabelText&&b.customLabelText[e.id]?b.customLabelText[e.id]:e.id}),"bar"})}function k(b,c,d){function e(a){return"undefined"!=typeof a&&"undefined"!=typeof a.latitude&&"undefined"!=typeof a.longitude}var f=this,g=this.options.fills,h=this.options.filters,i=this.svg;if(!c||c&&!c.slice)throw"Datamaps Error - bubbles must be an array";var j=b.selectAll("circle.datamaps-bubble").data(c,d.key);j.enter().append("svg:circle").attr("class","datamaps-bubble").attr("cx",function(a){var b;if(e(a)?b=f.latLngToXY(a.latitude,a.longitude):a.centered&&(b="USA"===a.centered?f.projection([-98.58333,39.83333]):f.path.centroid(i.select("path."+a.centered).data()[0])),b)return b[0]}).attr("cy",function(a){var b;if(e(a)?b=f.latLngToXY(a.latitude,a.longitude):a.centered&&(b="USA"===a.centered?f.projection([-98.58333,39.83333]):f.path.centroid(i.select("path."+a.centered).data()[0])),b)return b[1]}).attr("r",function(b){return d.animate?0:a(b.radius,d.radius,b)}).attr("data-info",function(a){return JSON.stringify(a)}).attr("filter",function(b){var c=h[a(b.filterKey,d.filterKey,b)];if(c)return c}).style("stroke",function(b){return a(b.borderColor,d.borderColor,b)}).style("stroke-width",function(b){return a(b.borderWidth,d.borderWidth,b)}).style("stroke-opacity",function(b){return a(b.borderOpacity,d.borderOpacity,b)}).style("fill-opacity",function(b){return a(b.fillOpacity,d.fillOpacity,b)}).style("fill",function(b){var c=g[a(b.fillKey,d.fillKey,b)];return c||g.defaultFill}).on("mouseover",function(b){var c=n.select(this);if(d.highlightOnHover){var e={fill:c.style("fill"),stroke:c.style("stroke"),"stroke-width":c.style("stroke-width"),"fill-opacity":c.style("fill-opacity")};c.style("fill",a(b.highlightFillColor,d.highlightFillColor,b)).style("stroke",a(b.highlightBorderColor,d.highlightBorderColor,b)).style("stroke-width",a(b.highlightBorderWidth,d.highlightBorderWidth,b)).style("stroke-opacity",a(b.highlightBorderOpacity,d.highlightBorderOpacity,b)).style("fill-opacity",a(b.highlightFillOpacity,d.highlightFillOpacity,b)).attr("data-previousAttributes",JSON.stringify(e))}d.popupOnHover&&f.updatePopup(c,b,d,i)}).on("mouseout",function(a){var b=n.select(this);if(d.highlightOnHover){var c=JSON.parse(b.attr("data-previousAttributes"));for(var e in c)b.style(e,c[e])}n.selectAll(".datamaps-hoverover").style("display","none")}),j.transition().duration(400).attr("r",function(b){return a(b.radius,d.radius,b)}).transition().duration(0).attr("data-info",function(a){return JSON.stringify(a)}),j.exit().transition().delay(d.exitDelay).attr("r",0).remove()}function l(a){return Array.prototype.slice.call(arguments,1).forEach(function(b){if(b)for(var c in b)null==a[c]&&("function"==typeof b[c]?a[c]=b[c]:a[c]=JSON.parse(JSON.stringify(b[c])))}),a}function m(a){if("undefined"==typeof n||"undefined"==typeof o)throw new Error("Include d3.js (v3.0.3 or greater) and topojson on this page before creating a new map");return this.options=l(a,p),this.options.geographyConfig=l(a.geographyConfig,p.geographyConfig),this.options.projectionConfig=l(a.projectionConfig,p.projectionConfig),this.options.bubblesConfig=l(a.bubblesConfig,p.bubblesConfig),this.options.arcConfig=l(a.arcConfig,p.arcConfig),n.select(this.options.element).select("svg").length>0&&b.call(this,this.options.element,this.options.height,this.options.width),this.addPlugin("bubbles",k),this.addPlugin("legend",g),this.addPlugin("arc",i),this.addPlugin("labels",j),this.addPlugin("graticule",h),this.options.disableDefaultStyles||d(),this.draw()}var n=window.d3,o=window.topojson,p={scope:"world",responsive:!1,aspectRatio:.5625,setProjection:c,projection:"equirectangular",dataType:"json",data:{},done:function(){},fills:{defaultFill:"#ABDDA4"},filters:{},geographyConfig:{dataUrl:null,hideAntarctica:!0,hideHawaiiAndAlaska:!1,borderWidth:1,borderOpacity:1,borderColor:"#FDFDFD",popupTemplate:function(a,b){return'
'+a.properties.name+"
"},popupOnHover:!0,highlightOnHover:!0,highlightFillColor:"#FC8D59",highlightBorderColor:"rgba(250, 15, 160, 0.2)",highlightBorderWidth:2,highlightBorderOpacity:1},projectionConfig:{rotation:[97,0]},bubblesConfig:{borderWidth:2,borderOpacity:1,borderColor:"#FFFFFF",popupOnHover:!0,radius:null,popupTemplate:function(a,b){return'
'+b.name+"
"},fillOpacity:.75,animate:!0,highlightOnHover:!0,highlightFillColor:"#FC8D59",highlightBorderColor:"rgba(250, 15, 160, 0.2)",highlightBorderWidth:2,highlightBorderOpacity:1,highlightFillOpacity:.85,exitDelay:100,key:JSON.stringify},arcConfig:{strokeColor:"#DD1C77",strokeWidth:1,arcSharpness:1,animationSpeed:600,popupOnHover:!1,popupTemplate:function(a,b){return b.origin&&b.destination&&b.origin.latitude&&b.origin.longitude&&b.destination.latitude&&b.destination.longitude?'
Arc
Origin: '+JSON.stringify(b.origin)+"
Destination: "+JSON.stringify(b.destination)+"
":b.origin&&b.destination?'
Arc
'+b.origin+" -> "+b.destination+"
":""}}};m.prototype.resize=function(){var a=this,b=a.options;if(b.responsive){var c=b.element.clientWidth,d=n.select(b.element).select("svg").attr("data-width");n.select(b.element).select("svg").selectAll("g").attr("transform","scale("+c/d+")")}},m.prototype.draw=function(){function a(a){b.options.dataUrl&&n[b.options.dataType](b.options.dataUrl,function(a){if("csv"===b.options.dataType&&a&&a.slice){for(var c={},d=0;d>>1;n[o]1){var c,a=[],s={LineString:o,MultiLineString:i,Polygon:i,MultiPolygon:function(n){n.forEach(i)}};u(t),a.forEach(arguments.length<3?function(n){f.push(n[0].i)}:function(n){r(n[0].g,n[n.length-1].g)&&f.push(n[0].i)})}else for(var l=0,h=n.arcs.length;l1)for(var u,f,c=1,a=e(i[0]);ca&&(f=i[0],i[0]=i[c],i[c]=f,a=u);return i})}}function l(n,t){return n[1][2]-t[1][2]}var h=function(){},p=function(n,t){return"GeometryCollection"===t.type?{type:"FeatureCollection",features:t.geometries.map(function(t){return i(n,t)})}:i(n,t)},v=function(n,t){function r(t){var r,e=n.arcs[t<0?~t:t],o=e[0];return n.transform?(r=[0,0],e.forEach(function(n){r[0]+=n[0],r[1]+=n[1]})):r=e[e.length-1],t<0?[r,o]:[o,r]}function e(n,t){for(var r in n){var e=n[r];delete t[e.start],delete e.start,delete e.end,e.forEach(function(n){o[n<0?~n:n]=1}),f.push(e)}}var o={},i={},u={},f=[],c=-1;return t.forEach(function(r,e){var o,i=n.arcs[r<0?~r:r];i.length<3&&!i[1][0]&&!i[1][1]&&(o=t[++c],t[c]=r,t[e]=o)}),t.forEach(function(n){var t,e,o=r(n),f=o[0],c=o[1];if(t=u[f])if(delete u[t.end],t.push(n),t.end=c,e=i[c]){delete i[e.start];var a=e===t?t:t.concat(e);i[a.start=t.start]=u[a.end=e.end]=a}else i[t.start]=u[t.end]=t;else if(t=i[c])if(delete i[t.start],t.unshift(n),t.start=f,e=u[f]){delete u[e.end];var s=e===t?t:e.concat(t);i[s.start=e.start]=u[s.end=t.end]=s}else i[t.start]=u[t.end]=t;else t=[n],i[t.start=f]=u[t.end=c]=t}),e(u,i),e(i,u),t.forEach(function(n){o[n<0?~n:n]||f.push([n])}),f},g=function(n){return u(n,f.apply(this,arguments))},d=function(n){return u(n,s.apply(this,arguments))},y=function(n){function t(n,t){n.forEach(function(n){n<0&&(n=~n);var r=i[n];r?r.push(t):i[n]=[t]})}function r(n,r){n.forEach(function(n){t(n,r)})}function e(n,t){"GeometryCollection"===n.type?n.geometries.forEach(function(n){e(n,t)}):n.type in f&&f[n.type](n.arcs,t)}var i={},u=n.map(function(){return[]}),f={LineString:t,MultiLineString:r,Polygon:r,MultiPolygon:function(n,t){n.forEach(function(n){r(n,t)})}};n.forEach(e);for(var c in i)for(var a=i[c],s=a.length,l=0;l0;){var r=(t+1>>1)-1,o=e[r];if(l(n,o)>=0)break;e[o._=t]=o,e[n._=t=r]=n}}function t(n,t){for(;;){var r=t+1<<1,i=r-1,u=t,f=e[u];if(i0&&(n=e[o],t(e[n._=0]=n,0)),r}},r.remove=function(r){var i,u=r._;if(e[u]===r)return u!==--o&&(i=e[o],(l(i,r)<0?n:t)(e[i._=u]=i,u)),u},r},E=function(n,e){function o(n){f.remove(n),n[1][2]=e(n),f.push(n)}var i=t(n.transform),u=r(n.transform),f=m();return null==e&&(e=c),n.arcs.forEach(function(n){var t,r,c,a,s=[],l=0;for(r=0,c=n.length;r public_users.id) +# +one: + user: one + token: MyString + revoked_at: 2025-05-20 14:00:56 + +two: + user: two + token: MyString + revoked_at: 2025-05-20 14:00:56 diff --git a/test/models/public/api_key_test.rb b/test/models/public/api_key_test.rb new file mode 100644 index 0000000..a6c9225 --- /dev/null +++ b/test/models/public/api_key_test.rb @@ -0,0 +1,29 @@ +# == Schema Information +# +# Table name: public_api_keys +# +# id :bigint not null, primary key +# name :string +# revoked_at :datetime +# token_bidx :string +# token_ciphertext :string +# created_at :datetime not null +# updated_at :datetime not null +# public_user_id :bigint not null +# +# Indexes +# +# index_public_api_keys_on_public_user_id (public_user_id) +# index_public_api_keys_on_token_bidx (token_bidx) UNIQUE +# +# Foreign Keys +# +# fk_rails_... (public_user_id => public_users.id) +# +require "test_helper" + +class Public::APIKeyTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/policies/customs_receipt_policy_test.rb b/test/policies/customs_receipt_policy_test.rb new file mode 100644 index 0000000..ecee7ca --- /dev/null +++ b/test/policies/customs_receipt_policy_test.rb @@ -0,0 +1,18 @@ +require 'test_helper' + +class CustomsReceiptPolicyTest < ActiveSupport::TestCase + def test_scope + end + + def test_show + end + + def test_create + end + + def test_update + end + + def test_destroy + end +end diff --git a/tmp/.keep b/tmp/.keep new file mode 100644 index 0000000..e69de29 diff --git a/vendor/.keep b/vendor/.keep new file mode 100644 index 0000000..e69de29 diff --git a/vite.config.mts b/vite.config.mts new file mode 100644 index 0000000..c632a45 --- /dev/null +++ b/vite.config.mts @@ -0,0 +1,31 @@ +import { defineConfig } from 'vite' +import ViteRails from 'vite-plugin-rails' + +export default defineConfig({ + plugins: [ + ViteRails(), + ], + define: { + 'this': 'globalThis', + 'global': 'globalThis', + }, + css: { + // postcss: './postcss.config.js', + preprocessorOptions: { + scss: { + api: 'modern-compiler' // or "modern" + } + } + }, + resolve: { + alias: { + '@': './app/frontend' + } + }, + build: { + target: 'esnext' //browsers can handle the latest ES features + }, + optimizeDeps: { + include: ['d3', 'datamaps'] + } +}) diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..043c3f3 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,2000 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"98.css@^0.1.21": + version "0.1.21" + resolved "https://registry.npmjs.org/98.css/-/98.css-0.1.21.tgz" + integrity sha512-ddk5qtUWyapM0Bzd5jwGExoE5fdSEGrP+F5VbYjyZLf2c9UVmn6w2NPTvCsoD4BWdGsjdLjlkQGhWwWTJcYQJQ== + +"@bufbuild/protobuf@^2.0.0": + version "2.2.5" + resolved "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.5.tgz" + integrity sha512-/g5EzJifw5GF8aren8wZ/G5oMuPoGeS6MQD3ca8ddcvdXR5UELUfdTZITCGNhNXynY/AYl3Z4plmxdj/tRl/hQ== + +"@esbuild/aix-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" + integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== + +"@esbuild/android-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" + integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== + +"@esbuild/android-arm@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" + integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== + +"@esbuild/android-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" + integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== + +"@esbuild/darwin-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz" + integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== + +"@esbuild/darwin-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" + integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== + +"@esbuild/freebsd-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" + integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== + +"@esbuild/freebsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" + integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== + +"@esbuild/linux-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" + integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== + +"@esbuild/linux-arm@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" + integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== + +"@esbuild/linux-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" + integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== + +"@esbuild/linux-loong64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" + integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== + +"@esbuild/linux-mips64el@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" + integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== + +"@esbuild/linux-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" + integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== + +"@esbuild/linux-riscv64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" + integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== + +"@esbuild/linux-s390x@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" + integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== + +"@esbuild/linux-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" + integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== + +"@esbuild/netbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" + integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== + +"@esbuild/openbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" + integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== + +"@esbuild/sunos-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" + integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== + +"@esbuild/win32-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" + integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== + +"@esbuild/win32-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" + integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== + +"@esbuild/win32-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" + integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== + +"@hotwired/stimulus@^3.2.2": + version "3.2.2" + resolved "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.2.2.tgz" + integrity sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A== + +"@hotwired/turbo-rails@^8.0.13": + version "8.0.13" + resolved "https://registry.npmjs.org/@hotwired/turbo-rails/-/turbo-rails-8.0.13.tgz" + integrity sha512-6SCnnOSzhtaJ0pNkAjncZxjtKsK3sP/vPEkCnTXBXSHkr+vF7DTZkOlwjhms1DbbQNTsjCsBoKvzSMbh/omSCQ== + dependencies: + "@hotwired/turbo" "^8.0.13" + "@rails/actioncable" "^7.0" + +"@hotwired/turbo@^8.0.13": + version "8.0.13" + resolved "https://registry.npmjs.org/@hotwired/turbo/-/turbo-8.0.13.tgz" + integrity sha512-M7qXUqcGab6G5PKOiwhgbByTtrPgKPFCTMNQ52QhzUEXEqmp0/ApEguUesh/FPiUjrmFec+3lq98KsWnYY2C7g== + +"@nathanvda/cocoon@^1.2.14": + version "1.2.14" + resolved "https://registry.npmjs.org/@nathanvda/cocoon/-/cocoon-1.2.14.tgz" + integrity sha512-WcEt2vVp50de2i7rkD4O+96O1iMtMIcTBNGPocrHfcmHDujKOngoLHFF8Ektgoh8PjwFAJMxx8WyGv0BtKTjxQ== + dependencies: + jquery "^3.3.1" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@oddcamp/cocoon-vanilla-js@^1.1.3": + version "1.1.3" + resolved "https://registry.npmjs.org/@oddcamp/cocoon-vanilla-js/-/cocoon-vanilla-js-1.1.3.tgz" + integrity sha512-zvi0GZznB0Wf/pC3M68sZVcnIxJwe785tqBoinBWHWgGHRwXZ96WJgyBrTBTDOkbgU2Tp40U/VZaDMVAofqLmw== + +"@open-iframe-resizer/core@^1.4.3": + version "1.4.3" + resolved "https://registry.yarnpkg.com/@open-iframe-resizer/core/-/core-1.4.3.tgz#18ae9f4860b90c75e3db0922d5d3f725f313cec7" + integrity sha512-wf3jqOGMDsu2NQyA7597B1lDa7hkboU13Cmw7PxjMw0FBGBtqj/gv1HWJlx4V4B3hnI9U0R31ZBBMFDV/ArdcQ== + +"@picocss/pico@^2.1.1": + version "2.1.1" + resolved "https://registry.npmjs.org/@picocss/pico/-/pico-2.1.1.tgz" + integrity sha512-kIDugA7Ps4U+2BHxiNHmvgPIQDWPDU4IeU6TNRdvXQM1uZX+FibqDQT2xUOnnO2yq/LUHcwnGlu1hvf4KfXnMg== + +"@rails/actioncable@^7.0": + version "7.2.201" + resolved "https://registry.npmjs.org/@rails/actioncable/-/actioncable-7.2.201.tgz" + integrity sha512-wsTdWoZ5EfG5k3t7ORdyQF0ZmDEgN4aVPCanHAiNEwCROqibSZMXXmCbH7IDJUVri4FOeAVwwbPINI7HVHPKBw== + +"@rollup/plugin-node-resolve@^16.0.0": + version "16.0.0" + resolved "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz" + integrity sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg== + dependencies: + "@rollup/pluginutils" "^5.0.1" + "@types/resolve" "1.20.2" + deepmerge "^4.2.2" + is-module "^1.0.0" + resolve "^1.22.1" + +"@rollup/pluginutils@^5.0.1": + version "5.1.4" + resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz" + integrity sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ== + dependencies: + "@types/estree" "^1.0.0" + estree-walker "^2.0.2" + picomatch "^4.0.2" + +"@rollup/rollup-android-arm-eabi@4.34.9": + version "4.34.9" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.9.tgz#661a45a4709c70e59e596ec78daa9cb8b8d27604" + integrity sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA== + +"@rollup/rollup-android-arm-eabi@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz#d964ee8ce4d18acf9358f96adc408689b6e27fe3" + integrity sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg== + +"@rollup/rollup-android-arm64@4.34.9": + version "4.34.9" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.9.tgz#128fe8dd510d880cf98b4cb6c7add326815a0c4b" + integrity sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg== + +"@rollup/rollup-android-arm64@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz#9b5e130ecc32a5fc1e96c09ff371743ee71a62d3" + integrity sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w== + +"@rollup/rollup-darwin-arm64@4.34.9": + version "4.34.9" + resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz" + integrity sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ== + +"@rollup/rollup-darwin-arm64@4.40.0": + version "4.40.0" + resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz" + integrity sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ== + +"@rollup/rollup-darwin-x64@4.34.9": + version "4.34.9" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz#c2fe3d85fffe47f0ed0f076b3563ada22c8af19c" + integrity sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q== + +"@rollup/rollup-darwin-x64@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz#d7380c1531ab0420ca3be16f17018ef72dd3d504" + integrity sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA== + +"@rollup/rollup-freebsd-arm64@4.34.9": + version "4.34.9" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.9.tgz#d95bd8f6eaaf829781144fc8bd2d5d71d9f6a9f5" + integrity sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw== + +"@rollup/rollup-freebsd-arm64@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz#cbcbd7248823c6b430ce543c59906dd3c6df0936" + integrity sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg== + +"@rollup/rollup-freebsd-x64@4.34.9": + version "4.34.9" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.9.tgz#c3576c6011656e4966ded29f051edec636b44564" + integrity sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g== + +"@rollup/rollup-freebsd-x64@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz#96bf6ff875bab5219c3472c95fa6eb992586a93b" + integrity sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw== + +"@rollup/rollup-linux-arm-gnueabihf@4.34.9": + version "4.34.9" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.9.tgz#48c87d0dee4f8dc9591a416717f91b4a89d77e3d" + integrity sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg== + +"@rollup/rollup-linux-arm-gnueabihf@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz#d80cd62ce6d40f8e611008d8dbf03b5e6bbf009c" + integrity sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA== + +"@rollup/rollup-linux-arm-musleabihf@4.34.9": + version "4.34.9" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.9.tgz#f4c4e7c03a7767f2e5aa9d0c5cfbf5c0f59f2d41" + integrity sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA== + +"@rollup/rollup-linux-arm-musleabihf@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz#75440cfc1e8d0f87a239b4c31dfeaf4719b656b7" + integrity sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg== + +"@rollup/rollup-linux-arm64-gnu@4.34.9": + version "4.34.9" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz#1015c9d07a99005025d13b8622b7600029d0b52f" + integrity sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw== + +"@rollup/rollup-linux-arm64-gnu@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz#ac527485ecbb619247fb08253ec8c551a0712e7c" + integrity sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg== + +"@rollup/rollup-linux-arm64-musl@4.34.9": + version "4.34.9" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz#8f895eb5577748fc75af21beae32439626e0a14c" + integrity sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A== + +"@rollup/rollup-linux-arm64-musl@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz#74d2b5cb11cf714cd7d1682e7c8b39140e908552" + integrity sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ== + +"@rollup/rollup-linux-loongarch64-gnu@4.34.9": + version "4.34.9" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.9.tgz#c9cd5dbbdc6b3ca4dbeeb0337498cf31949004a0" + integrity sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg== + +"@rollup/rollup-linux-loongarch64-gnu@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz#a0a310e51da0b5fea0e944b0abd4be899819aef6" + integrity sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg== + +"@rollup/rollup-linux-powerpc64le-gnu@4.34.9": + version "4.34.9" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.9.tgz#7ebb5b4441faa17843a210f7d0583a20c93b40e4" + integrity sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA== + +"@rollup/rollup-linux-powerpc64le-gnu@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz#4077e2862b0ac9f61916d6b474d988171bd43b83" + integrity sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw== + +"@rollup/rollup-linux-riscv64-gnu@4.34.9": + version "4.34.9" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.9.tgz#10f5d7349fbd2fe78f9e36ecc90aab3154435c8d" + integrity sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg== + +"@rollup/rollup-linux-riscv64-gnu@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz#5812a1a7a2f9581cbe12597307cc7ba3321cf2f3" + integrity sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA== + +"@rollup/rollup-linux-riscv64-musl@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz#973aaaf4adef4531375c36616de4e01647f90039" + integrity sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ== + +"@rollup/rollup-linux-s390x-gnu@4.34.9": + version "4.34.9" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.9.tgz#196347d2fa20593ab09d0b7e2589fb69bdf742c6" + integrity sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ== + +"@rollup/rollup-linux-s390x-gnu@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz#9bad59e907ba5bfcf3e9dbd0247dfe583112f70b" + integrity sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw== + +"@rollup/rollup-linux-x64-gnu@4.34.9": + version "4.34.9" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz#7193cbd8d128212b8acda37e01b39d9e96259ef8" + integrity sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A== + +"@rollup/rollup-linux-x64-gnu@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz#68b045a720bd9b4d905f462b997590c2190a6de0" + integrity sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ== + +"@rollup/rollup-linux-x64-musl@4.34.9": + version "4.34.9" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz#29a6867278ca0420b891574cfab98ecad70c59d1" + integrity sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA== + +"@rollup/rollup-linux-x64-musl@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz#8e703e2c2ad19ba7b2cb3d8c3a4ad11d4ee3a282" + integrity sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw== + +"@rollup/rollup-win32-arm64-msvc@4.34.9": + version "4.34.9" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz#89427dcac0c8e3a6d32b13a03a296a275d0de9a9" + integrity sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q== + +"@rollup/rollup-win32-arm64-msvc@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz#c5bee19fa670ff5da5f066be6a58b4568e9c650b" + integrity sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ== + +"@rollup/rollup-win32-ia32-msvc@4.34.9": + version "4.34.9" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.9.tgz#ecb9711ba2b6d2bf6ee51265abe057ab90913deb" + integrity sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w== + +"@rollup/rollup-win32-ia32-msvc@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz#846e02c17044bd922f6f483a3b4d36aac6e2b921" + integrity sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA== + +"@rollup/rollup-win32-x64-msvc@4.34.9": + version "4.34.9" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz#1973871850856ae72bc678aeb066ab952330e923" + integrity sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw== + +"@rollup/rollup-win32-x64-msvc@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz#fd92d31a2931483c25677b9c6698106490cbbc76" + integrity sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ== + +"@selectize/selectize@^0.15.2": + version "0.15.2" + resolved "https://registry.npmjs.org/@selectize/selectize/-/selectize-0.15.2.tgz" + integrity sha512-gY+yzYfrVTc+1ekCAaEtDvN59+upbibFzhkePyyk6PwOXT6kEb05azGA91/w3C/71lUOHPyd3nzLnfyfuRi+pA== + optionalDependencies: + jquery-ui "^1.13.2" + +"@tsparticles/basic@3.8.1": + version "3.8.1" + resolved "https://registry.npmjs.org/@tsparticles/basic/-/basic-3.8.1.tgz" + integrity sha512-my114zRmekT/+I2cGuEnHxlX5G/jO0iVtNnsxxlsgspXUTSY+fDixmrNF4UgFkuaIwd9Bv/yH+7S/4HE4qte7A== + dependencies: + "@tsparticles/engine" "3.8.1" + "@tsparticles/move-base" "3.8.1" + "@tsparticles/plugin-hex-color" "3.8.1" + "@tsparticles/plugin-hsl-color" "3.8.1" + "@tsparticles/plugin-rgb-color" "3.8.1" + "@tsparticles/shape-circle" "3.8.1" + "@tsparticles/updater-color" "3.8.1" + "@tsparticles/updater-opacity" "3.8.1" + "@tsparticles/updater-out-modes" "3.8.1" + "@tsparticles/updater-size" "3.8.1" + +"@tsparticles/confetti@^3.8.1": + version "3.8.1" + resolved "https://registry.npmjs.org/@tsparticles/confetti/-/confetti-3.8.1.tgz" + integrity sha512-ahtLeGiPwTXiWFAEnJHTOIFteYwjRJDpPEr4ohMSEHWD8Y0HkgtLQMzWgWMGYH/Q+ibi6DR5RGhXq6ndWkj/ag== + dependencies: + "@tsparticles/basic" "3.8.1" + "@tsparticles/engine" "3.8.1" + "@tsparticles/plugin-emitters" "3.8.1" + "@tsparticles/plugin-motion" "3.8.1" + "@tsparticles/shape-cards" "3.8.1" + "@tsparticles/shape-emoji" "3.8.1" + "@tsparticles/shape-heart" "3.8.1" + "@tsparticles/shape-image" "3.8.1" + "@tsparticles/shape-polygon" "3.8.1" + "@tsparticles/shape-square" "3.8.1" + "@tsparticles/shape-star" "3.8.1" + "@tsparticles/updater-life" "3.8.1" + "@tsparticles/updater-roll" "3.8.1" + "@tsparticles/updater-rotate" "3.8.1" + "@tsparticles/updater-tilt" "3.8.1" + "@tsparticles/updater-wobble" "3.8.1" + +"@tsparticles/engine@3.8.1": + version "3.8.1" + resolved "https://registry.npmjs.org/@tsparticles/engine/-/engine-3.8.1.tgz" + integrity sha512-S8h10nuZfElY7oih//NUHnT5qf4v3/dnsU8CMs7dz5lBEGr3amrYrXk0V+YKPTIQwfdmJHUaSBoAqFiv4aEGIA== + +"@tsparticles/move-base@3.8.1": + version "3.8.1" + resolved "https://registry.npmjs.org/@tsparticles/move-base/-/move-base-3.8.1.tgz" + integrity sha512-DNFRL1QT8ZQYLg3fIk74EbHJq5HGOq9CM2bCci9dDcdymvN4L7aWVFQavRiWDbi3y1EUW3+jeHSMbD3qHAfOeA== + dependencies: + "@tsparticles/engine" "3.8.1" + +"@tsparticles/plugin-emitters@3.8.1": + version "3.8.1" + resolved "https://registry.npmjs.org/@tsparticles/plugin-emitters/-/plugin-emitters-3.8.1.tgz" + integrity sha512-PGldE3OHs1hsZM6a8qHpXvKIMhaWAqZNwq8v7FwgJGxikXVvYtkKSaWslTpID3hYvtB6+whKig2uWURmq2TUsg== + dependencies: + "@tsparticles/engine" "3.8.1" + +"@tsparticles/plugin-hex-color@3.8.1": + version "3.8.1" + resolved "https://registry.npmjs.org/@tsparticles/plugin-hex-color/-/plugin-hex-color-3.8.1.tgz" + integrity sha512-AmgB7XIYBCvg5HcqYb19YpcjEx2k4DpU2e24n0rradDDeqKKcz7EWI/08FlAnDb5HUs1em63vaAanl1vdm3+OA== + dependencies: + "@tsparticles/engine" "3.8.1" + +"@tsparticles/plugin-hsl-color@3.8.1": + version "3.8.1" + resolved "https://registry.npmjs.org/@tsparticles/plugin-hsl-color/-/plugin-hsl-color-3.8.1.tgz" + integrity sha512-Ja6oEX6yu0064e4a+Fv1TBJiG5y0hqWwoOKSqf/Ra/zo01ageOEvDVX70FOVSrP+iEPGPznKVNcZs1tEOOvO0g== + dependencies: + "@tsparticles/engine" "3.8.1" + +"@tsparticles/plugin-motion@3.8.1": + version "3.8.1" + resolved "https://registry.npmjs.org/@tsparticles/plugin-motion/-/plugin-motion-3.8.1.tgz" + integrity sha512-UL/C1OKlysSdNf4ZMz8va1IaGbFuAAZE2hbkEE8B9It0N6Su3SyZtfssSDDQGAU4UQLmwRFd1HQ1M452X7d5+A== + dependencies: + "@tsparticles/engine" "3.8.1" + +"@tsparticles/plugin-rgb-color@3.8.1": + version "3.8.1" + resolved "https://registry.npmjs.org/@tsparticles/plugin-rgb-color/-/plugin-rgb-color-3.8.1.tgz" + integrity sha512-xNLqnaFUYjU+7dCHQXjZdM4UojUAVorPVmXlYmkh1xmujLljEaFTwCg1UJVlNq+fXENIFkeaf3/XT0U/q0ZBTA== + dependencies: + "@tsparticles/engine" "3.8.1" + +"@tsparticles/shape-cards@3.8.1": + version "3.8.1" + resolved "https://registry.npmjs.org/@tsparticles/shape-cards/-/shape-cards-3.8.1.tgz" + integrity sha512-RpShXeYERR23mxoMOMQfCuewZveH0+3c4IvJCJFzXLadGTLdfbZyhCZVv1+v2nS9rDAacnRK7hJRwM2qOph4TA== + dependencies: + "@tsparticles/engine" "3.8.1" + +"@tsparticles/shape-circle@3.8.1": + version "3.8.1" + resolved "https://registry.npmjs.org/@tsparticles/shape-circle/-/shape-circle-3.8.1.tgz" + integrity sha512-dM/f+qcpd8/KfviuVuKiTS8KLDE/T7xxHK7EI2S49yPW6yrJJBXdL7T4N9/n/6PF+Wslcl+kf/eTDjEAI3WjNQ== + dependencies: + "@tsparticles/engine" "3.8.1" + +"@tsparticles/shape-emoji@3.8.1": + version "3.8.1" + resolved "https://registry.npmjs.org/@tsparticles/shape-emoji/-/shape-emoji-3.8.1.tgz" + integrity sha512-xiXNZ/afdecengUXhOqgUwR+vysgaseVpzEjoGoliOMWq4WHWv+S6ujNfes2oz3x736mTlvKdXcEWRncSXaKWw== + dependencies: + "@tsparticles/engine" "3.8.1" + +"@tsparticles/shape-heart@3.8.1": + version "3.8.1" + resolved "https://registry.npmjs.org/@tsparticles/shape-heart/-/shape-heart-3.8.1.tgz" + integrity sha512-mUk3ZMp/f7FXDL3LseaDCTDaJSnWdja/MGwUOaGPNoyr0GwjeRxYUB0aSo+clpUiFEjJKAuzbgnGOc+AXokpvw== + dependencies: + "@tsparticles/engine" "3.8.1" + +"@tsparticles/shape-image@3.8.1": + version "3.8.1" + resolved "https://registry.npmjs.org/@tsparticles/shape-image/-/shape-image-3.8.1.tgz" + integrity sha512-7Yi25uLXvcY5A6TzyVBjYPsTmeTrE+0a2YO8kdp3O7V9NRGCSfXKnPRFp+lNOTiQRRvOG+SSzx2G18dfc/jwQg== + dependencies: + "@tsparticles/engine" "3.8.1" + +"@tsparticles/shape-polygon@3.8.1": + version "3.8.1" + resolved "https://registry.npmjs.org/@tsparticles/shape-polygon/-/shape-polygon-3.8.1.tgz" + integrity sha512-1pAx85NJbgmsOngl+ZAYH8vxwPJmoddjWCbWTD0wlp/x+2NRjn1iaGBKObPKLgwVzsAXb9qNHMsUX/x0C54svw== + dependencies: + "@tsparticles/engine" "3.8.1" + +"@tsparticles/shape-square@3.8.1": + version "3.8.1" + resolved "https://registry.npmjs.org/@tsparticles/shape-square/-/shape-square-3.8.1.tgz" + integrity sha512-4cjDt6542dkc15zxG1VYT7ScgPXM3+5VGtwMfh5CYNBx+GZZ3R+XUo1Q66JadcqKcNdHXfMWbXCMxs0GaiTtSw== + dependencies: + "@tsparticles/engine" "3.8.1" + +"@tsparticles/shape-star@3.8.1": + version "3.8.1" + resolved "https://registry.npmjs.org/@tsparticles/shape-star/-/shape-star-3.8.1.tgz" + integrity sha512-wBxnawqan/ocguNxY6cOEXF+YVnLIUmGBlnVGYx/7U9E2UHuHEKkoumob4fUflKISjvj5eQLpm/E1eUfYMd6RA== + dependencies: + "@tsparticles/engine" "3.8.1" + +"@tsparticles/updater-color@3.8.1": + version "3.8.1" + resolved "https://registry.npmjs.org/@tsparticles/updater-color/-/updater-color-3.8.1.tgz" + integrity sha512-HKrZzrF8YJ+TD+FdIwaWOPV565bkBhe+Ewj7CwKblG7H/SG+C6n1xIYobXkGP5pYkkQ+Cm1UV/Aq0Ih7sa+rJg== + dependencies: + "@tsparticles/engine" "3.8.1" + +"@tsparticles/updater-life@3.8.1": + version "3.8.1" + resolved "https://registry.npmjs.org/@tsparticles/updater-life/-/updater-life-3.8.1.tgz" + integrity sha512-5rCFFKD7js1lKgTpKOLo2OfmisWp4qqMVUVR4bNPeR0Ne/dcwDbKDzWyYS2AMsvWv/gcTTtWiarRfAiVQ5HtNg== + dependencies: + "@tsparticles/engine" "3.8.1" + +"@tsparticles/updater-opacity@3.8.1": + version "3.8.1" + resolved "https://registry.npmjs.org/@tsparticles/updater-opacity/-/updater-opacity-3.8.1.tgz" + integrity sha512-41dJ0T7df7AUFFkV9yU0buUfUwh+hLYcViXxkDy+6CJiiNCNZ4H404w1DTpBQLL4fbxUcDk9BXZLV7gkE2OfAw== + dependencies: + "@tsparticles/engine" "3.8.1" + +"@tsparticles/updater-out-modes@3.8.1": + version "3.8.1" + resolved "https://registry.npmjs.org/@tsparticles/updater-out-modes/-/updater-out-modes-3.8.1.tgz" + integrity sha512-BY8WqQwoDFpgPybwTzBU2GnxtRkjWnGStqBnR53x5+f1j7geTSY6WjcOvl1W+IkjtwtjiifriwBl41EbqMrjdQ== + dependencies: + "@tsparticles/engine" "3.8.1" + +"@tsparticles/updater-roll@3.8.1": + version "3.8.1" + resolved "https://registry.npmjs.org/@tsparticles/updater-roll/-/updater-roll-3.8.1.tgz" + integrity sha512-KYFTfMr8/M5pYBJFUFVrkogJURtKO5ogNSocOCf0v2QLMsbT5+OKNO7CLtxPZD98vTGRD3CHlt53/PF0tSesDA== + dependencies: + "@tsparticles/engine" "3.8.1" + +"@tsparticles/updater-rotate@3.8.1": + version "3.8.1" + resolved "https://registry.npmjs.org/@tsparticles/updater-rotate/-/updater-rotate-3.8.1.tgz" + integrity sha512-gpI07H1+diuuUdhJsQ1RlfHSD3fzBJrjyuwGuoXgHmvKzak6EWKpYfUMOraH4Dm41m/4kJZelle4nST+NpIuoA== + dependencies: + "@tsparticles/engine" "3.8.1" + +"@tsparticles/updater-size@3.8.1": + version "3.8.1" + resolved "https://registry.npmjs.org/@tsparticles/updater-size/-/updater-size-3.8.1.tgz" + integrity sha512-SC2ZxewtpwKadCalotK6x2YanxRO3hTMW1Rxzx9V2rcjAIgh/Nw49Vuithy2TDq8RtTc9rHDAPic2vMQ/lYQwA== + dependencies: + "@tsparticles/engine" "3.8.1" + +"@tsparticles/updater-tilt@3.8.1": + version "3.8.1" + resolved "https://registry.npmjs.org/@tsparticles/updater-tilt/-/updater-tilt-3.8.1.tgz" + integrity sha512-qMVd/sjrAds8m6vXFH5YKN8zrQR9SLdn5N5EvHx/JuKpOut4NhG85u8AEJL6ct1g7hY8Zj9kfi/dDSSovkaHhw== + dependencies: + "@tsparticles/engine" "3.8.1" + +"@tsparticles/updater-wobble@3.8.1": + version "3.8.1" + resolved "https://registry.npmjs.org/@tsparticles/updater-wobble/-/updater-wobble-3.8.1.tgz" + integrity sha512-PkjVgeSkW0EebJQ9PdpwSMWU2fAvKsVSuH4KGmodYlgGkH0/zvKjMOPMEI6YRAor1/vF1soFyLYp9Vax7Ae13g== + dependencies: + "@tsparticles/engine" "3.8.1" + +"@types/d3@3.5.38": + version "3.5.38" + resolved "https://registry.yarnpkg.com/@types/d3/-/d3-3.5.38.tgz#76f8f2e9159ae562965b2fa0e6fbee1aa643a1bc" + integrity sha512-O/gRkjWULp3xVX8K85V0H3tsSGole0WYt77KVpGZO2xTGLuVFuvE6JIsIli3fvFHCYBhGFn/8OHEEyMYF+QehA== + +"@types/estree@1.0.6", "@types/estree@^1.0.0": + version "1.0.6" + resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== + +"@types/estree@1.0.7": + version "1.0.7" + resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz" + integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== + +"@types/resolve@1.20.2": + version "1.20.2" + resolved "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz" + integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q== + +acorn@^7.1.1: + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + +ansicolors@~0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/ansicolors/-/ansicolors-0.2.1.tgz" + integrity sha512-tOIuy1/SK/dr94ZA0ckDohKXNeBNqZ4us6PjMVLs5h1w2GBB6uPtOknp2+VF4F/zcy9LI70W+Z+pE2Soajky1w== + +async@^2.6.0: + version "2.6.4" + resolved "https://registry.npmjs.org/async/-/async-2.6.4.tgz" + integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== + dependencies: + lodash "^4.17.14" + +braces@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +brfs@^1.3.0: + version "1.6.1" + resolved "https://registry.yarnpkg.com/brfs/-/brfs-1.6.1.tgz#b78ce2336d818e25eea04a0947cba6d4fb8849c3" + integrity sha512-OfZpABRQQf+Xsmju8XE9bDjs+uU4vLREGolP7bDgcpsI17QREyZ4Bl+2KLxxx1kCgA0fAIhKQBaBYh+PEcCqYQ== + dependencies: + quote-stream "^1.0.1" + resolve "^1.1.5" + static-module "^2.2.0" + through2 "^2.0.0" + +buffer-builder@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz" + integrity sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg== + +buffer-equal@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b" + integrity sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA== + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +cardinal@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/cardinal/-/cardinal-1.0.0.tgz" + integrity sha512-INsuF4GyiFLk8C91FPokbKTc/rwHqV4JnfatVZ6GPhguP1qmkRWX2dp5tepYboYdPpGWisLVLI+KsXoXFPRSMg== + dependencies: + ansicolors "~0.2.1" + redeyed "~1.0.0" + +colorjs.io@^0.5.0: + version "0.5.2" + resolved "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz" + integrity sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw== + +commander@2: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@7: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + +concat-stream@~1.6.0: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +convert-source-map@^1.5.1: + version "1.9.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +csv-parse@^4.6.5: + version "4.16.3" + resolved "https://registry.npmjs.org/csv-parse/-/csv-parse-4.16.3.tgz" + integrity sha512-cO1I/zmz4w2dcKHVvpCr7JVRu8/FymG5OEpmvsZYlccYolPBLoVGKUHgNoc4ZGkFeFlWGEDmMyBM+TTqRdW/wg== + +"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.2.0: + version "3.2.4" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5" + integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== + dependencies: + internmap "1 - 2" + +d3-axis@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-3.0.0.tgz#c42a4a13e8131d637b745fc2973824cfeaf93322" + integrity sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw== + +d3-brush@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-3.0.0.tgz#6f767c4ed8dcb79de7ede3e1c0f89e63ef64d31c" + integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ== + dependencies: + d3-dispatch "1 - 3" + d3-drag "2 - 3" + d3-interpolate "1 - 3" + d3-selection "3" + d3-transition "3" + +d3-chord@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-3.0.1.tgz#d156d61f485fce8327e6abf339cb41d8cbba6966" + integrity sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g== + dependencies: + d3-path "1 - 3" + +"d3-color@1 - 3", d3-color@3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" + integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== + +d3-contour@4: + version "4.0.2" + resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-4.0.2.tgz#bb92063bc8c5663acb2422f99c73cbb6c6ae3bcc" + integrity sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA== + dependencies: + d3-array "^3.2.0" + +d3-delaunay@6: + version "6.0.4" + resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz#98169038733a0a5babbeda55054f795bb9e4a58b" + integrity sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A== + dependencies: + delaunator "5" + +"d3-dispatch@1 - 3", d3-dispatch@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e" + integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== + +"d3-drag@2 - 3", d3-drag@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba" + integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== + dependencies: + d3-dispatch "1 - 3" + d3-selection "3" + +"d3-dsv@1 - 3", d3-dsv@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-3.0.1.tgz#c63af978f4d6a0d084a52a673922be2160789b73" + integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q== + dependencies: + commander "7" + iconv-lite "0.6" + rw "1" + +"d3-ease@1 - 3", d3-ease@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" + integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== + +d3-fetch@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-3.0.1.tgz#83141bff9856a0edb5e38de89cdcfe63d0a60a22" + integrity sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw== + dependencies: + d3-dsv "1 - 3" + +d3-force@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4" + integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg== + dependencies: + d3-dispatch "1 - 3" + d3-quadtree "1 - 3" + d3-timer "1 - 3" + +"d3-format@1 - 3", d3-format@3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" + integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== + +d3-geo-projection@0.2: + version "0.2.16" + resolved "https://registry.yarnpkg.com/d3-geo-projection/-/d3-geo-projection-0.2.16.tgz#4994ecd1033ddb1533b6c4c5528a1c81dcc29427" + integrity sha512-NB4/NRMnfJnpodvRbNY/nOzuoU17P229ASYf2l1GwjZyfD7l5aIuMylDMbIBF4y42BGZZvGdUwFW8iFM/5UBzg== + dependencies: + brfs "^1.3.0" + +d3-geo@3: + version "3.1.1" + resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-3.1.1.tgz#6027cf51246f9b2ebd64f99e01dc7c3364033a4d" + integrity sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q== + dependencies: + d3-array "2.5.0 - 3" + +d3-hierarchy@3: + version "3.1.2" + resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6" + integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA== + +"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== + dependencies: + d3-color "1 - 3" + +"d3-path@1 - 3", d3-path@3, d3-path@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" + integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== + +d3-polygon@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-3.0.1.tgz#0b45d3dd1c48a29c8e057e6135693ec80bf16398" + integrity sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg== + +"d3-quadtree@1 - 3", d3-quadtree@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f" + integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw== + +d3-queue@1: + version "1.2.3" + resolved "https://registry.yarnpkg.com/d3-queue/-/d3-queue-1.2.3.tgz#143a701cfa65fe021292f321c10d14e98abd491b" + integrity sha512-m6KtxX4V5pmVf1PqhH4SkQVMshSJfyCLM2vf2oFPi9FWFVT3+rtbCGerk766b/JXymHQDU3oqXHaZoiQ/e8yUQ== + +d3-queue@2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/d3-queue/-/d3-queue-2.0.3.tgz#07fbda3acae5358a9c5299aaf880adf0953ed2c2" + integrity sha512-ejbdHqZYEmk9ns/ljSbEcD6VRiuNwAkZMdFf6rsUb3vHROK5iMFd8xewDQnUVr6m/ba2BG63KmR/LySfsluxbg== + +d3-random@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-3.0.1.tgz#d4926378d333d9c0bfd1e6fa0194d30aebaa20f4" + integrity sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ== + +d3-scale-chromatic@3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz#34c39da298b23c20e02f1a4b239bd0f22e7f1314" + integrity sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ== + dependencies: + d3-color "1 - 3" + d3-interpolate "1 - 3" + +d3-scale@4: + version "4.0.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" + integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== + dependencies: + d3-array "2.10.0 - 3" + d3-format "1 - 3" + d3-interpolate "1.2.0 - 3" + d3-time "2.1.1 - 3" + d3-time-format "2 - 4" + +"d3-selection@2 - 3", d3-selection@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" + integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== + +d3-shape@3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" + integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== + dependencies: + d3-path "^3.1.0" + +"d3-time-format@2 - 4", d3-time-format@4: + version "4.1.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" + integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== + dependencies: + d3-time "1 - 3" + +"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" + integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== + dependencies: + d3-array "2 - 3" + +"d3-timer@1 - 3", d3-timer@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== + +"d3-transition@2 - 3", d3-transition@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f" + integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w== + dependencies: + d3-color "1 - 3" + d3-dispatch "1 - 3" + d3-ease "1 - 3" + d3-interpolate "1 - 3" + d3-timer "1 - 3" + +d3-zoom@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3" + integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw== + dependencies: + d3-dispatch "1 - 3" + d3-drag "2 - 3" + d3-interpolate "1 - 3" + d3-selection "2 - 3" + d3-transition "2 - 3" + +d3@3, d3@^3.5.6: + version "3.5.17" + resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.17.tgz#bc46748004378b21a360c9fc7cf5231790762fb8" + integrity sha512-yFk/2idb8OHPKkbAL8QaOaqENNoMhIaSHZerk3oQsECwkObkCpJyjYwCe+OHiq6UEdhe1m8ZGARRRO3ljFjlKg== + +d3@^7.9.0: + version "7.9.0" + resolved "https://registry.yarnpkg.com/d3/-/d3-7.9.0.tgz#579e7acb3d749caf8860bd1741ae8d371070cd5d" + integrity sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA== + dependencies: + d3-array "3" + d3-axis "3" + d3-brush "3" + d3-chord "3" + d3-color "3" + d3-contour "4" + d3-delaunay "6" + d3-dispatch "3" + d3-drag "3" + d3-dsv "3" + d3-ease "3" + d3-fetch "3" + d3-force "3" + d3-format "3" + d3-geo "3" + d3-hierarchy "3" + d3-interpolate "3" + d3-path "3" + d3-polygon "3" + d3-quadtree "3" + d3-random "3" + d3-scale "4" + d3-scale-chromatic "3" + d3-selection "3" + d3-shape "3" + d3-time "3" + d3-time-format "4" + d3-timer "3" + d3-transition "3" + d3-zoom "3" + +datamaps@^0.5.9: + version "0.5.9" + resolved "https://registry.yarnpkg.com/datamaps/-/datamaps-0.5.9.tgz#2a775473aaab29b55025208b2245e840ecfd4fe1" + integrity sha512-GUXpO713URNzaExVUgBtqA5fr2UuxUG/fVitI04zEFHVL2FHSjd672alHq8E16oQqRNzF0m1bmx8WlTnDrGSqQ== + dependencies: + "@types/d3" "3.5.38" + d3 "^3.5.6" + topojson "^1.6.19" + +debug@^4.3, debug@^4.3.4: + version "4.4.0" + resolved "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + +deep-is@~0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + +delaunator@5: + version "5.0.1" + resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.1.tgz#39032b08053923e924d6094fe2cde1a99cc51278" + integrity sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw== + dependencies: + robust-predicates "^3.0.2" + +dreamland@^0.0.25: + version "0.0.25" + resolved "https://registry.npmjs.org/dreamland/-/dreamland-0.0.25.tgz" + integrity sha512-REhB1T/6do8R7nwa/B0+ie0tTWY97cLsfPwmmqSHOMCu2Qw+cVSWhqTDB1Boklqg9Z8qfRkmGU/rknu0R48QWw== + +duplexer2@~0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + integrity sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA== + dependencies: + readable-stream "^2.0.2" + +esbuild@^0.21.3: + version "0.21.5" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz" + integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== + optionalDependencies: + "@esbuild/aix-ppc64" "0.21.5" + "@esbuild/android-arm" "0.21.5" + "@esbuild/android-arm64" "0.21.5" + "@esbuild/android-x64" "0.21.5" + "@esbuild/darwin-arm64" "0.21.5" + "@esbuild/darwin-x64" "0.21.5" + "@esbuild/freebsd-arm64" "0.21.5" + "@esbuild/freebsd-x64" "0.21.5" + "@esbuild/linux-arm" "0.21.5" + "@esbuild/linux-arm64" "0.21.5" + "@esbuild/linux-ia32" "0.21.5" + "@esbuild/linux-loong64" "0.21.5" + "@esbuild/linux-mips64el" "0.21.5" + "@esbuild/linux-ppc64" "0.21.5" + "@esbuild/linux-riscv64" "0.21.5" + "@esbuild/linux-s390x" "0.21.5" + "@esbuild/linux-x64" "0.21.5" + "@esbuild/netbsd-x64" "0.21.5" + "@esbuild/openbsd-x64" "0.21.5" + "@esbuild/sunos-x64" "0.21.5" + "@esbuild/win32-arm64" "0.21.5" + "@esbuild/win32-ia32" "0.21.5" + "@esbuild/win32-x64" "0.21.5" + +escodegen@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17" + integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w== + dependencies: + esprima "^4.0.1" + estraverse "^5.2.0" + esutils "^2.0.2" + optionalDependencies: + source-map "~0.6.1" + +escodegen@~1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.9.1.tgz#dbae17ef96c8e4bedb1356f4504fa4cc2f7cb7e2" + integrity sha512-6hTjO1NAWkHnDk3OqQ4YrCuwwmGHL9S3nPlzBOUG/R44rda3wLNrfvQ5fkSGjyhHFKM7ALPKcKGrwvCLe0lC7Q== + dependencies: + esprima "^3.1.3" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + +esprima@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" + integrity sha512-AWwVMNxwhN8+NIPQzAQZCm7RkLC4RbM3B1OobMuyp3i+w73X57KCKaVIxaRZb+DYCojq7rspo+fmuQfAboyhFg== + +esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esprima@~3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/esprima/-/esprima-3.0.0.tgz" + integrity sha512-xoBq/MIShSydNZOkjkoCEjqod963yHNXTLC40ypBhop6yPqflPz/vTinmCfSrGcywVLnSftRf6a0kJLdFdzemw== + +estraverse@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +falafel@^2.1.0: + version "2.2.5" + resolved "https://registry.yarnpkg.com/falafel/-/falafel-2.2.5.tgz#3ccb4970a09b094e9e54fead2deee64b4a589d56" + integrity sha512-HuC1qF9iTnHDnML9YZAdCDQwT0yKl/U55K4XSUXqGAA2GLoafFgWRqdAbhWJxXaYD4pyoVxAJ8wH670jMpI9DQ== + dependencies: + acorn "^7.1.1" + isarray "^2.0.1" + +fast-glob@^3.3.2: + version "3.3.3" + resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.8" + +fast-levenshtein@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastq@^1.6.0: + version "1.19.1" + resolved "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz" + integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ== + dependencies: + reusify "^1.0.4" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.4.tgz#2eb2860e000011dae4f1406a86fe80e530fb2ec6" + integrity sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ== + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +humanize@^0.0.9: + version "0.0.9" + resolved "https://registry.npmjs.org/humanize/-/humanize-0.0.9.tgz" + integrity sha512-bvZZ7vXpr1RKoImjuQ45hJb5OvE2oJafHysiD/AL3nkqTZH2hFCjQ3YZfCd63FefDitbJze/ispUPP0gfDsT2Q== + +iconv-lite@0.2: + version "0.2.11" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.2.11.tgz#1ce60a3a57864a292d1321ff4609ca4bb965adc8" + integrity sha512-KhmFWgaQZY83Cbhi+ADInoUQ8Etn6BG5fikM9syeOjQltvR45h7cRKJ/9uvQEuD61I3Uju77yYce0/LhKVClQw== + +iconv-lite@0.6: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +immutable@^5.0.2: + version "5.1.1" + resolved "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz" + integrity sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg== + +inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +"internmap@1 - 2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" + integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== + +is-core-module@^2.16.0: + version "2.16.1" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== + dependencies: + hasown "^2.0.2" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.1: + version "4.0.3" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz" + integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +isarray@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +jquery-ui@^1.13.2: + version "1.14.1" + resolved "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.14.1.tgz" + integrity sha512-DhzsYH8VeIvOaxwi+B/2BCsFFT5EGjShdzOcm5DssWjtcpGWIMsn66rJciDA6jBruzNiLf1q0KvwMoX1uGNvnQ== + dependencies: + jquery ">=1.12.0 <5.0.0" + +"jquery@>=1.12.0 <5.0.0", jquery@^3.3.1, jquery@^3.7.1: + version "3.7.1" + resolved "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz" + integrity sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg== + +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +lodash@^4.17.14: + version "4.17.21" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +magic-string@^0.22.4: + version "0.22.5" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.5.tgz#8e9cf5afddf44385c1da5bc2a6a0dbd10b03657e" + integrity sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w== + dependencies: + vlq "^0.2.2" + +merge-source-map@1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.0.4.tgz#a5de46538dae84d4114cc5ea02b4772a6346701f" + integrity sha512-PGSmS0kfnTnMJCzJ16BLLCEe6oeYCamKFFdQKshi4BmM6FUwipjVOcBFGxqtQtirtAG4iZvHlqST9CpZKqlRjA== + dependencies: + source-map "^0.5.6" + +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.8: + version "4.0.8" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +microplugin@0.0.3: + version "0.0.3" + resolved "https://registry.npmjs.org/microplugin/-/microplugin-0.0.3.tgz" + integrity sha512-3wKXex4/iyALV0GX2juow66J9dabkEMgHeZAihdLTaRTzm0N+RubXioNPpfIQDPuBRxr3JbjNt7B0Lr/3yE9yQ== + +minimist@^1.1.3: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz" + integrity sha512-iotkTvxc+TwOm5Ieim8VnSNvCDjCK9S8G3scJ50ZthspSxa7jx50jkhYduuAtAjvfDUwSgOwf8+If99AlOEhyw== + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@^3.3.8: + version "3.3.11" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + +object-inspect@~1.4.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.4.1.tgz#37ffb10e71adaf3748d05f713b4c9452f402cbc4" + integrity sha512-wqdhLpfCUbEsoEwl3FXwGyv8ief1k/1aUdIPCqVnupM6e8l63BEJdiF/0swtn04/8p05tG/T0FrpTlfwvljOdw== + +optimist@0.3: + version "0.3.7" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.3.7.tgz#c90941ad59e4273328923074d2cf2e7cbc6ec0d9" + integrity sha512-TCx0dXQzVtSCg2OgY/bO9hjM9cV4XYx09TVK+s3+FhkjT6LovsLe+pPMzpWf+6yXK/hUizs2gUoTw3jHM0VaTQ== + dependencies: + wordwrap "~0.0.2" + +optimist@^0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz" + integrity sha512-snN4O4TkigujZphWLN0E//nQmm7790RYaE53DdL7ZYwee2D8DDo9/EyYiKUfN3rneWUjhJnueija3G9I2i0h3g== + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + +optionator@^0.8.1: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +picocolors@^1.0.0, picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +picomatch@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz" + integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== + +postcss@^8.4.43: + version "8.5.3" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz" + integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A== + dependencies: + nanoid "^3.3.8" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +quote-stream@^1.0.1, quote-stream@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/quote-stream/-/quote-stream-1.0.2.tgz#84963f8c9c26b942e153feeb53aae74652b7e0b2" + integrity sha512-kKr2uQ2AokadPjvTyKJQad9xELbZwYzWlNfI3Uz2j/ib5u6H9lDP7fUUR//rMycd0gv4Z5P1qXMfXR8YpIxrjQ== + dependencies: + buffer-equal "0.0.1" + minimist "^1.1.3" + through2 "^2.0.0" + +qz-tray@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/qz-tray/-/qz-tray-2.2.4.tgz#14f2e5eb14656dfc3da71fa20d1517906466427c" + integrity sha512-5beEVkqewsuB//B0p+1guP7w45VU6kILazYa+VePHy5k0U2EzmE8JnZ/06pspgVgeeKIJDKb/Uu4Cu6tGi1cIw== + +readable-stream@^2.0.2, readable-stream@^2.2.2, readable-stream@~2.3.3, readable-stream@~2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +redeyed@~1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/redeyed/-/redeyed-1.0.1.tgz" + integrity sha512-8eEWsNCkV2rvwKLS1Cvp5agNjMhwRe2um+y32B2+3LqOzg4C9BBPs6vzAfV16Ivb8B9HPNKIqd8OrdBws8kNlQ== + dependencies: + esprima "~3.0.0" + +resolve@^1.1.5, resolve@^1.22.1: + version "1.22.10" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz" + integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== + dependencies: + is-core-module "^2.16.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +reusify@^1.0.4: + version "1.1.0" + resolved "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz" + integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== + +robust-predicates@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771" + integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg== + +rollup-plugin-gzip@^3.1.0: + version "3.1.2" + resolved "https://registry.npmjs.org/rollup-plugin-gzip/-/rollup-plugin-gzip-3.1.2.tgz" + integrity sha512-9xemMyvCjkklgNpu6jCYqQAbvCLJzA2nilkiOGzFuXTUX3cXEFMwIhsIBRF7kTKD/SnZ1tNPcxFm4m4zJ3VfNQ== + +rollup@^4.20.0: + version "4.40.0" + resolved "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz" + integrity sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w== + dependencies: + "@types/estree" "1.0.7" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.40.0" + "@rollup/rollup-android-arm64" "4.40.0" + "@rollup/rollup-darwin-arm64" "4.40.0" + "@rollup/rollup-darwin-x64" "4.40.0" + "@rollup/rollup-freebsd-arm64" "4.40.0" + "@rollup/rollup-freebsd-x64" "4.40.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.40.0" + "@rollup/rollup-linux-arm-musleabihf" "4.40.0" + "@rollup/rollup-linux-arm64-gnu" "4.40.0" + "@rollup/rollup-linux-arm64-musl" "4.40.0" + "@rollup/rollup-linux-loongarch64-gnu" "4.40.0" + "@rollup/rollup-linux-powerpc64le-gnu" "4.40.0" + "@rollup/rollup-linux-riscv64-gnu" "4.40.0" + "@rollup/rollup-linux-riscv64-musl" "4.40.0" + "@rollup/rollup-linux-s390x-gnu" "4.40.0" + "@rollup/rollup-linux-x64-gnu" "4.40.0" + "@rollup/rollup-linux-x64-musl" "4.40.0" + "@rollup/rollup-win32-arm64-msvc" "4.40.0" + "@rollup/rollup-win32-ia32-msvc" "4.40.0" + "@rollup/rollup-win32-x64-msvc" "4.40.0" + fsevents "~2.3.2" + +rollup@^4.34.9: + version "4.34.9" + resolved "https://registry.npmjs.org/rollup/-/rollup-4.34.9.tgz" + integrity sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ== + dependencies: + "@types/estree" "1.0.6" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.34.9" + "@rollup/rollup-android-arm64" "4.34.9" + "@rollup/rollup-darwin-arm64" "4.34.9" + "@rollup/rollup-darwin-x64" "4.34.9" + "@rollup/rollup-freebsd-arm64" "4.34.9" + "@rollup/rollup-freebsd-x64" "4.34.9" + "@rollup/rollup-linux-arm-gnueabihf" "4.34.9" + "@rollup/rollup-linux-arm-musleabihf" "4.34.9" + "@rollup/rollup-linux-arm64-gnu" "4.34.9" + "@rollup/rollup-linux-arm64-musl" "4.34.9" + "@rollup/rollup-linux-loongarch64-gnu" "4.34.9" + "@rollup/rollup-linux-powerpc64le-gnu" "4.34.9" + "@rollup/rollup-linux-riscv64-gnu" "4.34.9" + "@rollup/rollup-linux-s390x-gnu" "4.34.9" + "@rollup/rollup-linux-x64-gnu" "4.34.9" + "@rollup/rollup-linux-x64-musl" "4.34.9" + "@rollup/rollup-win32-arm64-msvc" "4.34.9" + "@rollup/rollup-win32-ia32-msvc" "4.34.9" + "@rollup/rollup-win32-x64-msvc" "4.34.9" + fsevents "~2.3.2" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +rw@1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" + integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ== + +rxjs@^7.4.0: + version "7.8.2" + resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz" + integrity sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA== + dependencies: + tslib "^2.1.0" + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +"safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sass-embedded-android-arm64@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.86.3.tgz#daa4658a383e4834a511fd00321841b5da71fd7d" + integrity sha512-q+XwFp6WgAv+UgnQhsB8KQ95kppvWAB7DSoJp+8Vino8b9ND+1ai3cUUZPE5u4SnLZrgo5NtrbPvN5KLc4Pfyg== + +sass-embedded-android-arm@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-android-arm/-/sass-embedded-android-arm-1.86.3.tgz#adf63d572e972aaba07b6dc3a006828ed745b4d1" + integrity sha512-UyeXrFzZSvrGbvrWUBcspbsbivGgAgebLGJdSqJulgSyGbA6no3DWQ5Qpdd6+OAUC39BlpPu74Wx9s4RrVuaFw== + +sass-embedded-android-ia32@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-android-ia32/-/sass-embedded-android-ia32-1.86.3.tgz#daca4191cf0e4625e79e6765ced132106ff2641e" + integrity sha512-gTJjVh2cRzvGujXj5ApPk/owUTL5SiO7rDtNLrzYAzi1N5HRuLYXqk3h1IQY3+eCOBjGl7mQ9XyySbJs/3hDvg== + +sass-embedded-android-riscv64@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.86.3.tgz#b62bc4ca759c3d3bff545bd1eaa85e462392bfd4" + integrity sha512-Po3JnyiCS16kd6REo1IMUbFGYtvL9O0rmKaXx5vOuBaJD1LPy2LiSSp7TU7wkJ9IxsTDGzFaSeP1I9qb6D8VVg== + +sass-embedded-android-x64@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-android-x64/-/sass-embedded-android-x64-1.86.3.tgz#5440c91eae7db2b281e414f27e331d7556dac0d4" + integrity sha512-+7h3jdDv/0kUFx0BvxYlq2fa7CcHiDPlta6k5OxO5K6jyqJwo9hc0Z052BoYEauWTqZ+vK6bB5rv2BIzq4U9nA== + +sass-embedded-darwin-arm64@1.86.3: + version "1.86.3" + resolved "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.86.3.tgz" + integrity sha512-EgLwV4ORm5Hr0DmIXo0Xw/vlzwLnfAiqD2jDXIglkBsc5czJmo4/IBdGXOP65TRnsgJEqvbU3aQhuawX5++x9A== + +sass-embedded-darwin-x64@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.86.3.tgz#ea9a7c694ede309b3daf95262dda0681e9de973c" + integrity sha512-dfKhfrGPRNLWLC82vy/vQGmNKmAiKWpdFuWiePRtg/E95pqw+sCu6080Y6oQLfFu37Iq3MpnXiSpDuSo7UnPWA== + +sass-embedded-linux-arm64@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.86.3.tgz#0472365e17f57086f5006056d19e597a1b147fec" + integrity sha512-tYq5rywR53Qtc+0KI6pPipOvW7a47ETY69VxfqI9BR2RKw2hBbaz0bIw6OaOgEBv2/XNwcWb7a4sr7TqgkqKAA== + +sass-embedded-linux-arm@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.86.3.tgz#fcc85a2ad5bf335197a16c33992fe4c9c59807ed" + integrity sha512-+fVCIH+OR0SMHn2NEhb/VfbpHuUxcPtqMS34OCV3Ka99LYZUJZqth4M3lT/ppGl52mwIVLNYzR4iLe6mdZ6mYA== + +sass-embedded-linux-ia32@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-ia32/-/sass-embedded-linux-ia32-1.86.3.tgz#80ccbf951c1a9a816ce460595208686f13d078f0" + integrity sha512-CmQ5OkqnaeLdaF+bMqlYGooBuenqm3LvEN9H8BLhjkpWiFW8hnYMetiqMcJjhrXLvDw601KGqA5sr/Rsg5s45g== + +sass-embedded-linux-musl-arm64@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.86.3.tgz#664d7178017b2b47983fcf7bcdad03d90ec9109a" + integrity sha512-4zOr2C/eW89rxb4ozTfn7lBzyyM5ZigA1ZSRTcAR26Qbg/t2UksLdGnVX9/yxga0d6aOi0IvO/7iM2DPPRRotg== + +sass-embedded-linux-musl-arm@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.86.3.tgz#d3eace3ac4804541372ed61ce9aee384e3f22945" + integrity sha512-SEm65SQknI4pl+mH5Xf231hOkHJyrlgh5nj4qDbiBG6gFeutaNkNIeRgKEg3cflXchCr8iV/q/SyPgjhhzQb7w== + +sass-embedded-linux-musl-ia32@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-ia32/-/sass-embedded-linux-musl-ia32-1.86.3.tgz#755eb08baa6da277bcd8b642710c7ffa16930586" + integrity sha512-84Tcld32LB1loiqUvczWyVBQRCChm0wNLlkT59qF29nxh8njFIVf9yaPgXcSyyjpPoD9Tu0wnq3dvVzoMCh9AQ== + +sass-embedded-linux-musl-riscv64@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.86.3.tgz#d6e9b0c45b23be340999cc384eda04ae9fe34043" + integrity sha512-IxEqoiD7vdNpiOwccybbV93NljBy64wSTkUOknGy21SyV43C8uqESOwTwW9ywa3KufImKm8L3uQAW/B0KhJMWg== + +sass-embedded-linux-musl-x64@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.86.3.tgz#88d6e6dcf1d9ac76c7e9949e2613310918a02617" + integrity sha512-ePeTPXUxPK6JgHcUfnrkIyDtyt+zlAvF22mVZv6y1g/PZFm1lSfX+Za7TYHg9KaYqaaXDiw6zICX4i44HhR8rA== + +sass-embedded-linux-riscv64@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.86.3.tgz#624725ba3322f49b2401df6abb912e55879da526" + integrity sha512-NuXQ72dwfNLe35E+RaXJ4Noq4EkFwM65eWwCwxEWyJO9qxOx1EXiCAJii6x8kkOh5daWuMU0VAI1B9RsJaqqQQ== + +sass-embedded-linux-x64@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.86.3.tgz#ac61d48784f794c0ee752a25d7105b1cb3c3a979" + integrity sha512-t8be9zJ5B82+og9bQmIQ83yMGYZMTMrlGA+uGWtYacmwg6w3093dk91Fx0YzNSZBp3Tk60qVYjCZnEIwy60x0g== + +sass-embedded-win32-arm64@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.86.3.tgz#d71186bfbf16e2051ae145ea53f4cdc0f1db231d" + integrity sha512-4ghuAzjX4q8Nksm0aifRz8hgXMMxS0SuymrFfkfJlrSx68pIgvAge6AOw0edoZoe0Tf5ZbsWUWamhkNyNxkTvw== + +sass-embedded-win32-ia32@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-win32-ia32/-/sass-embedded-win32-ia32-1.86.3.tgz#5e85820c515fce300d770950d776e0c68d72001e" + integrity sha512-tCaK4zIRq9mLRPxLzBAdYlfCuS/xLNpmjunYxeWkIwlJo+k53h1udyXH/FInnQ2GgEz0xMXyvH3buuPgzwWYsw== + +sass-embedded-win32-x64@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.86.3.tgz#4bfd3e6969823487ee9497923a033f2456ce9f65" + integrity sha512-zS+YNKfTF4SnOfpC77VTb0qNZyTXrxnAezSoRV0xnw6HlY+1WawMSSB6PbWtmbvyfXNgpmJUttoTtsvJjRCucg== + +sass-embedded@^1.86.3: + version "1.86.3" + resolved "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.86.3.tgz" + integrity sha512-3pZSp24ibO1hdopj+W9DuiWsZOb2YY6AFRo/jjutKLBkqJGM1nJjXzhAYfzRV+Xn5BX1eTI4bBTE09P0XNHOZg== + dependencies: + "@bufbuild/protobuf" "^2.0.0" + buffer-builder "^0.2.0" + colorjs.io "^0.5.0" + immutable "^5.0.2" + rxjs "^7.4.0" + supports-color "^8.1.1" + sync-child-process "^1.0.2" + varint "^6.0.0" + optionalDependencies: + sass-embedded-android-arm "1.86.3" + sass-embedded-android-arm64 "1.86.3" + sass-embedded-android-ia32 "1.86.3" + sass-embedded-android-riscv64 "1.86.3" + sass-embedded-android-x64 "1.86.3" + sass-embedded-darwin-arm64 "1.86.3" + sass-embedded-darwin-x64 "1.86.3" + sass-embedded-linux-arm "1.86.3" + sass-embedded-linux-arm64 "1.86.3" + sass-embedded-linux-ia32 "1.86.3" + sass-embedded-linux-musl-arm "1.86.3" + sass-embedded-linux-musl-arm64 "1.86.3" + sass-embedded-linux-musl-ia32 "1.86.3" + sass-embedded-linux-musl-riscv64 "1.86.3" + sass-embedded-linux-musl-x64 "1.86.3" + sass-embedded-linux-riscv64 "1.86.3" + sass-embedded-linux-x64 "1.86.3" + sass-embedded-win32-arm64 "1.86.3" + sass-embedded-win32-ia32 "1.86.3" + sass-embedded-win32-x64 "1.86.3" + +select2@^4.1.0-rc.0: + version "4.1.0-rc.0" + resolved "https://registry.npmjs.org/select2/-/select2-4.1.0-rc.0.tgz" + integrity sha512-Hr9TdhyHCZUtwznEH2CBf7967mEM0idtJ5nMtjvk3Up5tPukOLXbHUNmh10oRfeNIhj+3GD3niu+g6sVK+gK0A== + +selectize@^0.12.6: + version "0.12.6" + resolved "https://registry.npmjs.org/selectize/-/selectize-0.12.6.tgz" + integrity sha512-bWO5A7G+I8+QXyjLfQUgh31VI4WKYagUZQxAXlDyUmDDNrFxrASV0W9hxCOl0XJ/XQ1dZEu3G9HjXV4Wj0yb6w== + dependencies: + microplugin "0.0.3" + sifter "^0.5.1" + +shallow-copy@~0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/shallow-copy/-/shallow-copy-0.0.1.tgz#415f42702d73d810330292cc5ee86eae1a11a170" + integrity sha512-b6i4ZpVuUxB9h5gfCxPiusKYkqTMOjEbBs4wMaFbkfia4yFv92UKZ6Df8WXcKbn08JNL/abvg3FnMAOfakDvUw== + +shapefile@0.3: + version "0.3.1" + resolved "https://registry.yarnpkg.com/shapefile/-/shapefile-0.3.1.tgz#9bb9a429bd6086a0cfb03962d14cfdf420ffba12" + integrity sha512-BZoPvnq4ULce0pyKiZUU4D8CdPl0Z1fpE73AeCkwyMbD2hpUeVA0s7jIE/wX8uWNruVeJV6e+rznPHBwuH5J6g== + dependencies: + d3-queue "1" + iconv-lite "0.2" + optimist "0.3" + +sifter@^0.5.1: + version "0.5.4" + resolved "https://registry.npmjs.org/sifter/-/sifter-0.5.4.tgz" + integrity sha512-t2yxTi/MM/ESup7XH5oMu8PUcttlekt269RqxARgnvS+7D/oP6RyA1x3M/5w8dG9OgkOyQ8hNRWelQ8Rj4TAQQ== + dependencies: + async "^2.6.0" + cardinal "^1.0.0" + csv-parse "^4.6.5" + humanize "^0.0.9" + optimist "^0.6.1" + +source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +source-map@^0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== + +source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +static-eval@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/static-eval/-/static-eval-2.1.1.tgz#71ac6a13aa32b9e14c5b5f063c362176b0d584ba" + integrity sha512-MgWpQ/ZjGieSVB3eOJVs4OA2LT/q1vx98KPCTTQPzq/aLr0YUXTsgryTXr4SLfR0ZfUUCiedM9n/ABeDIyy4mA== + dependencies: + escodegen "^2.1.0" + +static-module@^2.2.0: + version "2.2.5" + resolved "https://registry.yarnpkg.com/static-module/-/static-module-2.2.5.tgz#bd40abceae33da6b7afb84a0e4329ff8852bfbbf" + integrity sha512-D8vv82E/Kpmz3TXHKG8PPsCPg+RAX6cbCOyvjM6x04qZtQ47EtJFVwRsdov3n5d6/6ynrOY9XB4JkaZwB2xoRQ== + dependencies: + concat-stream "~1.6.0" + convert-source-map "^1.5.1" + duplexer2 "~0.1.4" + escodegen "~1.9.0" + falafel "^2.1.0" + has "^1.0.1" + magic-string "^0.22.4" + merge-source-map "1.0.4" + object-inspect "~1.4.0" + quote-stream "~1.0.2" + readable-stream "~2.3.3" + shallow-copy "~0.0.1" + static-eval "^2.0.0" + through2 "~2.0.3" + +stimulus-vite-helpers@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/stimulus-vite-helpers/-/stimulus-vite-helpers-3.1.0.tgz" + integrity sha512-qy9vnNnu6e/1PArEndp456BuSKLQkBgc+vX2pedOHT0N4GSLQY0l5fuQ4ft56xZ8xSWqrfuYSR+GXXIPtoESww== + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +sync-child-process@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz" + integrity sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA== + dependencies: + sync-message-port "^1.0.0" + +sync-message-port@^1.0.0: + version "1.1.3" + resolved "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.1.3.tgz" + integrity sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg== + +through2@^2.0.0, through2@~2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +topojson-client@3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/topojson-client/-/topojson-client-3.1.0.tgz#22e8b1ed08a2b922feeb4af6f53b6ef09a467b99" + integrity sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw== + dependencies: + commander "2" + +topojson-client@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/topojson-client/-/topojson-client-3.0.0.tgz#1f99293a77ef42a448d032a81aa982b73f360d2f" + integrity sha512-2phZ98wg/iKvsWxbB6JQcq0/N0f+sRx8ZogdvjCg+CjaJdmV0knP0OQwK5XbgnytAPx5lPZk41kiWpgH2w9FHg== + dependencies: + commander "2" + +topojson-server@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/topojson-server/-/topojson-server-3.0.0.tgz#378e78e87c3972a7b5be2c5d604369b6bae69c5e" + integrity sha512-UhhwQk4e2+lwhAVYkja3J5nQHQmKwORDuIQPkMnFFZFcLqWKLQWI3u7fZWtNIXTElBjTYdBUL1kzi1+oS/qDQw== + dependencies: + commander "2" + +topojson-simplify@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/topojson-simplify/-/topojson-simplify-3.0.2.tgz#8a2403e639531500fafa0c6594e8b0fadebc2c02" + integrity sha512-gyYSVRt4jO/0RJXKZQPzTDQRWV+D/nOfiljNUv0HBXslFLtq3yxRHrl7jbrjdbda5Ytdr7M8BZUI4OxU7tnbRQ== + dependencies: + commander "2" + topojson-client "3" + +topojson@^1.6.19: + version "1.6.27" + resolved "https://registry.yarnpkg.com/topojson/-/topojson-1.6.27.tgz#adbe33a67e2f1673d338df12644ad20fc20b42ed" + integrity sha512-JLFtrhClUH/k/yvsiCXqcWcXaOfO3DgFvHnYb+gS2xlDbjbvkKh6YB1CPilmEV++tH33xw6wCxoYA5g6YLZw/Q== + dependencies: + d3 "3" + d3-geo-projection "0.2" + d3-queue "2" + optimist "0.3" + rw "1" + shapefile "0.3" + +topojson@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/topojson/-/topojson-3.0.2.tgz#fcb927306c3e0fa76656fa58deed4555d2346fb4" + integrity sha512-u3zeuL6WEVL0dmsRn7uHZKc4Ao4gpW3sORUv+N3ezLTvY3JdCuyg0hvpWiIfFw8p/JwVN++SvAsFgcFEeR15rQ== + dependencies: + topojson-client "3.0.0" + topojson-server "3.0.0" + topojson-simplify "3.0.2" + +tslib@^2.1.0: + version "2.8.1" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== + dependencies: + prelude-ls "~1.1.2" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +vanilla-nested@^1.7.1: + version "1.7.1" + resolved "https://registry.npmjs.org/vanilla-nested/-/vanilla-nested-1.7.1.tgz" + integrity sha512-z8nOa5n3bczhSno27TCABMc9qKjPcMP4iW6THqWxmtPZ8ZsFtoOl6XJGsqTvemkjSRpyFq4Zgqe7wZgCH9GHLw== + +varint@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz" + integrity sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg== + +vite-plugin-environment@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/vite-plugin-environment/-/vite-plugin-environment-1.1.3.tgz" + integrity sha512-9LBhB0lx+2lXVBEWxFZC+WO7PKEyE/ykJ7EPWCq95NEcCpblxamTbs5Dm3DLBGzwODpJMEnzQywJU8fw6XGGGA== + +vite-plugin-full-reload@^1.1.0: + version "1.2.0" + resolved "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.2.0.tgz" + integrity sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA== + dependencies: + picocolors "^1.0.0" + picomatch "^2.3.1" + +vite-plugin-manifest-sri@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/vite-plugin-manifest-sri/-/vite-plugin-manifest-sri-0.2.0.tgz" + integrity sha512-Zt5jt19xTIJ91LOuQTCtNG7rTFc5OziAjBz2H5NdCGqaOD1nxrWExLhcKW+W4/q8/jOPCg/n5ncYEQmqCxiGQQ== + +vite-plugin-rails@^0.5.0: + version "0.5.0" + resolved "https://registry.npmjs.org/vite-plugin-rails/-/vite-plugin-rails-0.5.0.tgz" + integrity sha512-PR3zTHW96X8c7dRsuL2Mu1EAXXeO8fQjQ2KanwIC7EWgBST+D8AKjJyEUAr13IakrIYvd1cM3LcQUcrKmCMePg== + dependencies: + rollup-plugin-gzip "^3.1.0" + vite-plugin-environment "^1.1.3" + vite-plugin-full-reload "^1.1.0" + vite-plugin-manifest-sri "^0.2.0" + vite-plugin-ruby "^5.0.0" + vite-plugin-stimulus-hmr "^3.0.0" + +vite-plugin-ruby@^5.0.0, vite-plugin-ruby@^5.1.0: + version "5.1.1" + resolved "https://registry.npmjs.org/vite-plugin-ruby/-/vite-plugin-ruby-5.1.1.tgz" + integrity sha512-I1dXJq2ywdvTD2Cz5LYNcYLujqQ3eUxPoCjruRdfm2QBtHBY15NEeb6x5HuPM3T5S+y8S3p9fwRsieQQCjk0gg== + dependencies: + debug "^4.3.4" + fast-glob "^3.3.2" + +vite-plugin-stimulus-hmr@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/vite-plugin-stimulus-hmr/-/vite-plugin-stimulus-hmr-3.0.0.tgz" + integrity sha512-KElOiZOlaG4XilQQHrzK8M1u5UfK4EFfADJKQYbnmsUMifDOnPR6anVYgHAN95QyWJ67Q/rYWe5BB9M5OxocfQ== + dependencies: + debug "^4.3" + stimulus-vite-helpers "^3.0.0" + +vite@^5.0.0: + version "5.4.18" + resolved "https://registry.npmjs.org/vite/-/vite-5.4.18.tgz" + integrity sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA== + dependencies: + esbuild "^0.21.3" + postcss "^8.4.43" + rollup "^4.20.0" + optionalDependencies: + fsevents "~2.3.3" + +vlq@^0.2.2: + version "0.2.3" + resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26" + integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow== + +word-wrap@~1.2.3: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz" + integrity sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw== + +xtend@~4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==