diff --git a/Gemfile b/Gemfile index 126b84aa..79d31939 100644 --- a/Gemfile +++ b/Gemfile @@ -35,8 +35,6 @@ gem "view_component" # Reduces boot times through caching; required in config/boot.rb gem "bootsnap", require: false - -gem 'dotenv-rails', groups: [:development, :test] gem "faker" # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] @@ -46,6 +44,7 @@ group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem "debug", platforms: %i[ mri windows ] gem "bullet" + gem 'dotenv-rails' end group :development do @@ -70,4 +69,5 @@ group :test do gem "timecop" gem "webmock" gem "vcr" + gem 'rails-controller-testing' end diff --git a/Gemfile.lock b/Gemfile.lock index 7ec1c3a0..ddea33a6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -213,6 +213,10 @@ GEM activesupport (= 7.1.2) bundler (>= 1.15.0) railties (= 7.1.2) + rails-controller-testing (1.0.5) + actionpack (>= 5.0.1.rc1) + actionview (>= 5.0.1.rc1) + activesupport (>= 5.0.1.rc1) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -345,6 +349,7 @@ DEPENDENCIES puma (>= 5.0) rack-cors rails (= 7.1.2) + rails-controller-testing rspec-rails selenium-webdriver shoulda-matchers (~> 5.0) diff --git a/app/components/clients/list_item_component.html.erb b/app/components/clients/list_item_component.html.erb new file mode 100644 index 00000000..7366b2e7 --- /dev/null +++ b/app/components/clients/list_item_component.html.erb @@ -0,0 +1,25 @@ +
  • +
    +
    +

    + <%= link_to client_path(@client) do %> + + <%= client_name %> + <% end %> +

    +

    + <%= client_description %> +

    +
    +
    +
    + + +
    +
  • \ No newline at end of file diff --git a/app/components/clients/list_item_component.rb b/app/components/clients/list_item_component.rb new file mode 100644 index 00000000..574a58fc --- /dev/null +++ b/app/components/clients/list_item_component.rb @@ -0,0 +1,20 @@ +module Clients + class ListItemComponent < ViewComponent::Base + def initialize(client:, current_company:) + @current_company = current_company + @client = client + end + + def client_name + @client.name + end + + def client_description + @client.description + end + + def client_status + @client.status + end + end +end \ No newline at end of file diff --git a/app/components/settings/breadcrumbs_component.rb b/app/components/settings/breadcrumbs_component.rb index 58955891..b2e4ec22 100644 --- a/app/components/settings/breadcrumbs_component.rb +++ b/app/components/settings/breadcrumbs_component.rb @@ -2,8 +2,5 @@ module Settings class BreadcrumbsComponent < ViewComponent::Base renders_many :breadcrumbs, Settings::BreadcrumbComponent - - def initialize() - end end end \ No newline at end of file diff --git a/app/components/settings/users/list_item_component.html.erb b/app/components/settings/users/list_item_component.html.erb index d2c30a7b..ff943388 100644 --- a/app/components/settings/users/list_item_component.html.erb +++ b/app/components/settings/users/list_item_component.html.erb @@ -21,7 +21,7 @@

    <%= user_job_title %>

    <% end %>
    - <%= render(Settings::Users::StatusComponent.new(status: user_status)) %> + <%= render(Shared::StatusComponent.new(status: user_status)) %>
    - Status: <%= user_status %> - \ No newline at end of file diff --git a/app/components/settings/users/status_component.rb b/app/components/settings/users/status_component.rb deleted file mode 100644 index 13598917..00000000 --- a/app/components/settings/users/status_component.rb +++ /dev/null @@ -1,22 +0,0 @@ -module Settings - module Users - class StatusComponent < ViewComponent::Base - def initialize(status:) - @status = status - end - - def user_status - @status.capitalize - end - - def user_status_color - case @status - when Membership::ACTIVE - "bg-green-50 text-green-700 ring-green-600/20" - else - "bg-gray-50 text-gray-600 ring-gray-500/10" - end - end - end - end -end \ No newline at end of file diff --git a/app/components/shared/status_component.html.erb b/app/components/shared/status_component.html.erb new file mode 100644 index 00000000..9962ef94 --- /dev/null +++ b/app/components/shared/status_component.html.erb @@ -0,0 +1,3 @@ + + Status: <%= target_status.titleize %> + \ No newline at end of file diff --git a/app/components/shared/status_component.rb b/app/components/shared/status_component.rb new file mode 100644 index 00000000..7aff9a72 --- /dev/null +++ b/app/components/shared/status_component.rb @@ -0,0 +1,20 @@ +module Shared + class StatusComponent < ViewComponent::Base + def initialize(status:) + @status = status + end + + def target_status + @status.capitalize + end + + def target_status_color + case @status + when Membership::ACTIVE || Client::ACTIVE + "bg-green-50 text-green-700 ring-green-600/20" + else + "bg-gray-50 text-gray-600 ring-gray-500/10" + end + end + end +end \ No newline at end of file diff --git a/app/controllers/clients_controller.rb b/app/controllers/clients_controller.rb index a0ec8e66..60d432c2 100644 --- a/app/controllers/clients_controller.rb +++ b/app/controllers/clients_controller.rb @@ -1,71 +1,55 @@ class ClientsController < ApplicationController before_action :require_user! - before_action :set_client, only: %i[ show edit update destroy ] + before_action :set_client, only: %i[ show edit update toggle_archived ] - # GET /clients or /clients.json def index - @clients = Client.all + @clients = current_company.clients.all end - # GET /clients/1 or /clients/1.json def show end - # GET /clients/new def new @client = Client.new end - # GET /clients/1/edit def edit end - # POST /clients or /clients.json def create - @client = Client.new(client_params) + @client = current_company.clients.new(create_client_params) - respond_to do |format| - if @client.save - format.html { redirect_to client_url(@client), notice: "Client was successfully created." } - format.json { render :show, status: :created, location: @client } - else - format.html { render :new, status: :unprocessable_entity } - format.json { render json: @client.errors, status: :unprocessable_entity } - end + if @client.save + redirect_to client_url(@client), notice: "Client was successfully created." + else + render :new, status: :unprocessable_entity end end - # PATCH/PUT /clients/1 or /clients/1.json def update - respond_to do |format| - if @client.update(client_params) - format.html { redirect_to client_url(@client), notice: "Client was successfully updated." } - format.json { render :show, status: :ok, location: @client } - else - format.html { render :edit, status: :unprocessable_entity } - format.json { render json: @client.errors, status: :unprocessable_entity } - end + if @client.update(update_client_params) + redirect_to client_url(@client), notice: "Client was successfully updated." + else + render :edit, status: :unprocessable_entity end end - # DELETE /clients/1 or /clients/1.json - def destroy - @client.destroy! + def toggle_archived + @client.toggle_archived! - respond_to do |format| - format.html { redirect_to clients_url, notice: "Client was successfully destroyed." } - format.json { head :no_content } - end + redirect_to clients_url, notice: "Client status was successfully updated." end private - # Use callbacks to share common setup or constraints between actions. def set_client - @client = Client.find(params[:id]) + @client = current_company.clients.find(params[:id]) + end + + def create_client_params + params.require(:client).permit(:name, :description) end - # Only allow a list of trusted parameters through. - def client_params - params.require(:client).permit(:name, :description, :status, :company_id) + def update_client_params + params.require(:client).permit(:name, :description, :status) end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 942ee10c..78adff8b 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,60 +1,43 @@ class ProjectsController < ApplicationController before_action :require_user! - before_action :set_project, only: %i[ show edit update destroy ] + before_action :set_project, only: %i[ show edit update ] - # GET /projects or /projects.json def index @projects = Project.includes(:company). all end - # GET /projects/1 or /projects/1.json def show end - # GET /projects/new def new - @project = Project.new + @client = Client.find_by(id: params[:client_id]) # optional + @project = Project.new( + client_id: params[:client_id], + + # TODO: make these configurable? + status: Project::PROPOSED, + payment_frequency: Project::MONTHLY + ) end - # GET /projects/1/edit def edit end - # POST /projects or /projects.json def create @project = Project.new(project_params) - respond_to do |format| - if @project.save - format.html { redirect_to project_url(@project), notice: "Project was successfully created." } - format.json { render :show, status: :created, location: @project } - else - format.html { render :new, status: :unprocessable_entity } - format.json { render json: @project.errors, status: :unprocessable_entity } - end + if @project.save + redirect_to project_url(@project), notice: "Project was successfully created." + else + render :new, status: :unprocessable_entity end end - # PATCH/PUT /projects/1 or /projects/1.json def update - respond_to do |format| - if @project.update(project_params) - format.html { redirect_to project_url(@project), notice: "Project was successfully updated." } - format.json { render :show, status: :ok, location: @project } - else - format.html { render :edit, status: :unprocessable_entity } - format.json { render json: @project.errors, status: :unprocessable_entity } - end - end - end - - # DELETE /projects/1 or /projects/1.json - def destroy - @project.destroy! - - respond_to do |format| - format.html { redirect_to projects_url, notice: "Project was successfully destroyed." } - format.json { head :no_content } + if @project.update(project_params) + redirect_to project_url(@project), notice: "Project was successfully updated." + else + render :edit, status: :unprocessable_entity end end diff --git a/app/models/client.rb b/app/models/client.rb index ff1a4b08..ea55ef17 100644 --- a/app/models/client.rb +++ b/app/models/client.rb @@ -13,4 +13,24 @@ class Client < ApplicationRecord scope :active, -> { where(status: 'active') } scope :archived, -> { where(status: 'archived') } + + def active? + status == ACTIVE + end + + def archived? + status == ARCHIVED + end + def toggle_archived! + new_status = active? ? Client::ARCHIVED : Client::ACTIVE + + Client.transaction do + update!(status: new_status) + + # archive all projects + if new_status == Client::ARCHIVED + projects.update_all(status: Project::ARCHIVED) + end + end + end end diff --git a/app/models/project.rb b/app/models/project.rb index 09837f3d..227814e0 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -28,4 +28,24 @@ class Project < ApplicationRecord validates :status, presence: true, inclusion: { in: VALID_STATUSES } validates :cost, presence: true, numericality: { greater_than_or_equal_to: 0.0 } validates :payment_frequency, presence: true, inclusion: { in: VALID_PAYMENT_FREQUENCIES } + + def active? + status == ACTIVE + end + + def archived? + status == ARCHIVED + end + + def proposed? + status == PROPOSED + end + + def cancelled? + status == CANCELLED + end + + def completed? + status == COMPLETED + end end diff --git a/app/views/clients/_client.html.erb b/app/views/clients/_client.html.erb deleted file mode 100644 index 9fe1013e..00000000 --- a/app/views/clients/_client.html.erb +++ /dev/null @@ -1,27 +0,0 @@ -
    -

    - Name: - <%= client.name %> -

    - -

    - Description: - <%= client.description %> -

    - -

    - Status: - <%= client.status %> -

    - -

    - Company: - <%= client.company_id %> -

    - - <% if action_name != "show" %> - <%= link_to "Show this client", client, class: "rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %> - <%= link_to "Edit this client", edit_client_path(client), class: "rounded-lg py-3 ml-2 px-5 bg-gray-100 inline-block font-medium" %> -
    - <% end %> -
    diff --git a/app/views/clients/_client.json.jbuilder b/app/views/clients/_client.json.jbuilder deleted file mode 100644 index 853686c9..00000000 --- a/app/views/clients/_client.json.jbuilder +++ /dev/null @@ -1,2 +0,0 @@ -json.extract! client, :id, :name, :description, :status, :company_id, :created_at, :updated_at -json.url client_url(client, format: :json) diff --git a/app/views/clients/_form.html.erb b/app/views/clients/_form.html.erb deleted file mode 100644 index 74595cb0..00000000 --- a/app/views/clients/_form.html.erb +++ /dev/null @@ -1,34 +0,0 @@ -<%= form_with(model: client, class: "contents") do |form| %> - <% if client.errors.any? %> -
    -

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

    - - -
    - <% end %> - -
    - <%= form.label :name %> - <%= form.text_field :name, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %> -
    - -
    - <%= form.label :description %> - <%= form.text_area :description, rows: 4, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %> -
    - -
    - <%= form.label :status %> - <%= form.select :status, Client::VALID_STATUSES, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %> -
    - - <%= form.hidden_field :company_id, value: current_company_id %> - -
    - <%= form.submit class: "rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer" %> -
    -<% end %> diff --git a/app/views/clients/edit.html.erb b/app/views/clients/edit.html.erb index 3c0c5f9f..4fa9a96c 100644 --- a/app/views/clients/edit.html.erb +++ b/app/views/clients/edit.html.erb @@ -1,8 +1,66 @@ -
    -

    Editing client

    +
    + <%= render(Settings::BreadcrumbsComponent.new) do |breadcrumbs_component| + breadcrumbs_component.with_breadcrumb(title: "Clients", link: clients_path, first: true) do |clients_icon| + clients_icon.with_svg do %> + + + + <% end + end + breadcrumbs_component.with_breadcrumb(title: @client.name, last: true) + end %> +
    - <%= render "form", client: @client %> +
    + <%= form_for @client, url: client_path(@client), data: {turbo: false} do |f| %> +
    +
    +

    Add a new client

    - <%= link_to "Show this client", @client, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %> - <%= link_to "Back to clients", clients_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %> -
    + <% if @client.errors.any? %> +
    +
    +
    + +
    +
    +

    There were <%= pluralize(@client.errors.count, "error") %> with adding the user to your company

    +
    +
      + <% @client.errors.full_messages.each do |error| %> +
    • <%= error %>
    • + <% end %> +
    +
    +
    +
    +
    + <% end %> + +
    +
    + <%= f.label :name, "Client name", class: "block text-sm font-medium leading-6 text-gray-900" %> +
    + <%= f.text_field :name, class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" %> +
    +
    + +
    + <%= f.label :email, "Description", class: "block text-sm font-medium leading-6 text-gray-900" %> +
    + <%= f.text_area :description, class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" %> +
    +
    +
    +
    +
    + +
    + <%= link_to "Cancel", client_path(@client), class: "text-sm font-semibold leading-6 text-gray-900" %> + <%= submit_tag "Save changes", data: {disable_with: "Please wait..."}, class: "rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" %> +
    + <% end %> + +
    \ No newline at end of file diff --git a/app/views/clients/index.html.erb b/app/views/clients/index.html.erb index 977e4da2..0c8eca72 100644 --- a/app/views/clients/index.html.erb +++ b/app/views/clients/index.html.erb @@ -1,14 +1,15 @@ -
    - <% if notice.present? %> -

    <%= notice %>

    - <% end %> - -
    -

    Clients

    - <%= link_to "New client", new_client_path, class: "rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium" %> -
    - -
    - <%= render @clients %> +
    +

    Clients

    +
    + <%= link_to "New Client", new_client_path, class: "ml-3 inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" %>
    + + +
    +
      + <% @clients.each do |client| %> + <%= render(Clients::ListItemComponent.new(client:, current_company:)) %> + <% end %> +
    +
    \ No newline at end of file diff --git a/app/views/clients/index.json.jbuilder b/app/views/clients/index.json.jbuilder deleted file mode 100644 index aca36bca..00000000 --- a/app/views/clients/index.json.jbuilder +++ /dev/null @@ -1 +0,0 @@ -json.array! @clients, partial: "clients/client", as: :client diff --git a/app/views/clients/new.html.erb b/app/views/clients/new.html.erb index 2c5aa7ca..4939f537 100644 --- a/app/views/clients/new.html.erb +++ b/app/views/clients/new.html.erb @@ -1,7 +1,62 @@ -
    -

    New client

    +
    + <%= link_to clients_path, class: "ml-3 inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" do %> + + + + Back + <% end %> +
    - <%= render "form", client: @client %> +
    + <%= form_for @client, url: clients_path, data: {turbo: false} do |f| %> +
    +
    +

    Add a new client

    - <%= link_to "Back to clients", clients_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %> -
    + <% if @client.errors.any? %> +
    +
    +
    + +
    +
    +

    There were <%= pluralize(@client.errors.count, "error") %> with adding the user to your company

    +
    +
      + <% @client.errors.full_messages.each do |error| %> +
    • <%= error %>
    • + <% end %> +
    +
    +
    +
    +
    + <% end %> + +
    +
    + <%= f.label :name, "Client name", class: "block text-sm font-medium leading-6 text-gray-900" %> +
    + <%= f.text_field :name, class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" %> +
    +
    + +
    + <%= f.label :email, "Description", class: "block text-sm font-medium leading-6 text-gray-900" %> +
    + <%= f.text_area :description, class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" %> +
    +
    +
    +
    +
    + +
    + <%= link_to "Cancel", clients_path, class: "text-sm font-semibold leading-6 text-gray-900" %> + <%= submit_tag "Create", data: {disable_with: "Please wait..."}, class: "rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" %> +
    + <% end %> + +
    \ No newline at end of file diff --git a/app/views/clients/show.html.erb b/app/views/clients/show.html.erb index 8f33bdaa..5599682d 100644 --- a/app/views/clients/show.html.erb +++ b/app/views/clients/show.html.erb @@ -1,15 +1,106 @@ -
    -
    - <% if notice.present? %> -

    <%= notice %>

    - <% end %> +
    + <%= render(Settings::BreadcrumbsComponent.new) do |breadcrumbs_component| + breadcrumbs_component.with_breadcrumb(title: "Clients", link: clients_path, first: true) do |clients_icon| + clients_icon.with_svg do %> + + + + <% end + end + breadcrumbs_component.with_breadcrumb(title: @client.name, last: true) + end %> - <%= render @client %> +
    + <%= link_to "Edit", edit_client_path(@client), class: "ml-3 inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" %> - <%= link_to "Edit this client", edit_client_path(@client), class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %> -
    - <%= button_to "Destroy this client", client_path(@client), method: :delete, class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 font-medium" %> -
    - <%= link_to "Back to clients", clients_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %> + <% link_text = @client.active? ? 'Archive' : 'Activate' %> + <% confirmation_text = @client.active? ? 'Are you sure? This will archive all projects for this client.' : "Are you sure?" %> + <%= button_to link_text, toggle_archived_client_path(@client), method: :post, data: { turbo_confirm: confirmation_text }, form_class: "inline", class: "ml-3 inline-flex items-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600" %>
    + +
    +
    +
    +
    Client name
    +
    + <%= @client.name %> +
    +
    +
    +
    Description
    +
    + <%= @client.description.presence || "N/A" %> +
    +
    +
    +
    Projects
    +
    +
      +
    • +
      + <%= link_to new_project_path(client_id: @client.id), class: "inline-flex items-center gap-x-1.5 rounded-md bg-indigo-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" do %> + + + + + New Project + <% end %> +
      +
    • + <% @client.projects.each do |project| %> +
    • +
      + <% if project.active? %> + + + Active + + + <% elsif project.proposed? %> + + + Proposed + + <% elsif project.archived? %> + + + Archived + + <% elsif project.cancelled? %> + + + Cancelled + + <% elsif project.completed? %> + + + Completed + + <% end %> +
      + + <%= link_to project.name, project_path(project) %> + +
      +
      +
      + <%= link_to "View StaffPlan", "#", class: "font-medium text-indigo-600 hover:text-indigo-500", onclick: "alert('this will take you to the React front end')" %> +
      +
    • + <% end %> +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/app/views/clients/show.json.jbuilder b/app/views/clients/show.json.jbuilder deleted file mode 100644 index bc3ac83a..00000000 --- a/app/views/clients/show.json.jbuilder +++ /dev/null @@ -1 +0,0 @@ -json.partial! "clients/client", client: @client diff --git a/app/views/dashboard/show.html.erb b/app/views/dashboard/show.html.erb index f0a1e069..abb8d7a5 100644 --- a/app/views/dashboard/show.html.erb +++ b/app/views/dashboard/show.html.erb @@ -18,15 +18,19 @@
  • <% if membership.active? %> - - - - Active + + + Active + <% else %> - - - - Inactive + + + Inactive + <% end %>
    diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index ade880d8..befdd643 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -26,7 +26,7 @@
    My StaffPlan - Clients + <%= header_link_to "Clients", clients_path %> Projects <% if current_user.owner?(company: current_company) %> <%= header_link_to "Settings", settings_path %> @@ -97,7 +97,7 @@