initial commit

This commit is contained in:
Stefan Tollkühn
2025-07-21 12:14:42 +02:00
parent a77fc87832
commit 3735122750
156 changed files with 3862 additions and 1 deletions

0
app/assets/images/.keep Normal file
View File

View File

@@ -0,0 +1,10 @@
/*
* This is a manifest file that'll be compiled into application.css.
*
* With Propshaft, assets are served efficiently without preprocessing steps. You can still include
* application-wide styles in this file, but keep in mind that CSS precedence will follow the standard
* cascading order, meaning styles declared later in the document or manifest will override earlier ones,
* depending on specificity.
*
* Consider organizing styles into separate files for maintainability.
*/

View File

@@ -0,0 +1,4 @@
class ApplicationController < ActionController::Base
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
allow_browser versions: :modern
end

View File

@@ -0,0 +1,70 @@
class ClientsController < ApplicationController
before_action :set_client, only: %i[ show edit update destroy ]
# GET /clients or /clients.json
def index
@clients = Client.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)
respond_to do |format|
if @client.save
format.html { redirect_to @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
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, 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
end
end
# DELETE /clients/1 or /clients/1.json
def destroy
@client.destroy!
respond_to do |format|
format.html { redirect_to clients_path, status: :see_other, notice: "Client was successfully destroyed." }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_client
@client = Client.find(params.expect(:id))
end
# Only allow a list of trusted parameters through.
def client_params
params.expect(client: [ :company_name, :firstname, :lastname, :streetname, :zipcode, :city, :country, :email, :phone ])
end
end

View File

View File

@@ -0,0 +1,84 @@
class ProjectsController < ApplicationController
before_action :set_project, only: %i[ show edit update destroy ]
# GET /projects or /projects.json
def index
@projects = Project.all
end
# GET /projects/1 or /projects/1.json
def show
end
# GET /projects/new
def new
@project = Project.new
@project.subprojects.build # initialize one subproject
@project.subprojects.each do |subproject|
subproject.build_client
subproject.build_owner
subproject.build_builder
end
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, 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
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, 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_path, status: :see_other, notice: "Project was successfully destroyed." }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_project
@project = Project.find(params.expect(:id))
end
# Only allow a list of trusted parameters through.
def project_params
params.require(:project).permit(
:name,
subprojects_attributes: [
:id, :subproject_name, :client_id, :owner_id, :builder_id, :_destroy,
client_attributes: [:company_name, :firstname, :lastname, :streetname, :zipcode, :city, :country, :email, :phone],
owner_attributes: [:company_name, :firstname, :lastname, :streetname, :zipcode, :city, :country, :email, :phone],
builder_attributes: [:company_name, :firstname, :lastname, :streetname, :zipcode, :city, :country, :email, :phone]
]
)
end
end

View File

@@ -0,0 +1,70 @@
class SubprojectsController < ApplicationController
before_action :set_subproject, only: %i[ show edit update destroy ]
# GET /subprojects or /subprojects.json
def index
@subprojects = Subproject.all
end
# GET /subprojects/1 or /subprojects/1.json
def show
end
# GET /subprojects/new
def new
@subproject = Subproject.new
end
# GET /subprojects/1/edit
def edit
end
# POST /subprojects or /subprojects.json
def create
@subproject = Subproject.new(subproject_params)
respond_to do |format|
if @subproject.save
format.html { redirect_to @subproject, notice: "Subproject was successfully created." }
format.json { render :show, status: :created, location: @subproject }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @subproject.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /subprojects/1 or /subprojects/1.json
def update
respond_to do |format|
if @subproject.update(subproject_params)
format.html { redirect_to @subproject, notice: "Subproject was successfully updated." }
format.json { render :show, status: :ok, location: @subproject }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @subproject.errors, status: :unprocessable_entity }
end
end
end
# DELETE /subprojects/1 or /subprojects/1.json
def destroy
@subproject.destroy!
respond_to do |format|
format.html { redirect_to subprojects_path, status: :see_other, notice: "Subproject was successfully destroyed." }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_subproject
@subproject = Subproject.find(params.expect(:id))
end
# Only allow a list of trusted parameters through.
def subproject_params
params.expect(subproject: [ :subproject_name, :project_id, :client_id, :owner_id, :builder_id ])
end
end

View File

@@ -0,0 +1,2 @@
module ApplicationHelper
end

View File

@@ -0,0 +1,2 @@
module ClientsHelper
end

View File

@@ -0,0 +1,2 @@
module ProjectsHelper
end

View File

@@ -0,0 +1,2 @@
module SubprojectsHelper
end

View File

@@ -0,0 +1,3 @@
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "@hotwired/turbo-rails"
import "controllers"

View File

@@ -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 }

View File

@@ -0,0 +1,7 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
this.element.textContent = "Hello World!"
}
}

View File

@@ -0,0 +1,4 @@
// Import and register all your controllers from the importmap via controllers/**/*_controller
import { application } from "controllers/application"
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
eagerLoadControllersFrom("controllers", application)

View File

@@ -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

View File

@@ -0,0 +1,4 @@
class ApplicationMailer < ActionMailer::Base
default from: "from@example.com"
layout "mailer"
end

View File

@@ -0,0 +1,3 @@
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
end

8
app/models/client.rb Normal file
View File

@@ -0,0 +1,8 @@
class Client < ApplicationRecord
has_many :client_subprojects, class_name: 'Subproject', foreign_key: 'client_id'
has_many :owner_subprojects, class_name: 'Subproject', foreign_key: 'owner_id'
has_many :builder_subprojects, class_name: 'Subproject', foreign_key: 'builder_id'
validates :company_name, :firstname, :lastname, presence: true
# Add other validations as needed
end

View File

6
app/models/project.rb Normal file
View File

@@ -0,0 +1,6 @@
class Project < ApplicationRecord
has_many :subprojects, inverse_of: :project
accepts_nested_attributes_for :subprojects, allow_destroy: true, reject_if: :all_blank
validates :name, presence: true
end

53
app/models/subproject.rb Normal file
View File

@@ -0,0 +1,53 @@
class Subproject < ApplicationRecord
belongs_to :project
belongs_to :client, class_name: 'Client', optional: true
belongs_to :owner, class_name: 'Client', optional: true
belongs_to :builder, class_name: 'Client', optional: true
accepts_nested_attributes_for :client, reject_if: :all_blank
accepts_nested_attributes_for :owner, reject_if: :all_blank
accepts_nested_attributes_for :builder, reject_if: :all_blank
validates :subproject_name, presence: true
validate :client_presence_check
validate :owner_presence_check
validate :builder_presence_check
private
private
def client_presence_check
if client_id.blank? && (client_attributes_blank?)
errors.add(:client, "must be selected or a new client must be provided")
end
end
def owner_presence_check
if owner_id.blank? && (owner_attributes_blank?)
errors.add(:owner, "must be selected or a new owner must be provided")
end
end
def builder_presence_check
if builder_id.blank? && (builder_attributes_blank?)
errors.add(:builder, "must be selected or a new builder must be provided")
end
end
def client_attributes_blank?
return true if client.nil?
client.attributes.except("id", "created_at", "updated_at").values.all?(&:blank?)
end
def owner_attributes_blank?
return true if client.nil?
owner_attributes.except("id", "created_at", "updated_at").values.all?(&:blank?)
end
def builder_attributes_blank?
return true if client.nil?
builder_attributes.except("id", "created_at", "updated_at").values.all?(&:blank?)
end
end

View File

@@ -0,0 +1,47 @@
<div id="<%= dom_id client %>">
<p>
<strong>Company name:</strong>
<%= client.company_name %>
</p>
<p>
<strong>Firstname:</strong>
<%= client.firstname %>
</p>
<p>
<strong>Lastname:</strong>
<%= client.lastname %>
</p>
<p>
<strong>Streetname:</strong>
<%= client.streetname %>
</p>
<p>
<strong>Zipcode:</strong>
<%= client.zipcode %>
</p>
<p>
<strong>City:</strong>
<%= client.city %>
</p>
<p>
<strong>Country:</strong>
<%= client.country %>
</p>
<p>
<strong>Email:</strong>
<%= client.email %>
</p>
<p>
<strong>Phone:</strong>
<%= client.phone %>
</p>
</div>

View File

@@ -0,0 +1,2 @@
json.extract! client, :id, :company_name, :firstname, :lastname, :streetname, :zipcode, :city, :country, :email, :phone, :created_at, :updated_at
json.url client_url(client, format: :json)

View File

@@ -0,0 +1,62 @@
<%= form_with(model: client) do |form| %>
<% if client.errors.any? %>
<div style="color: red">
<h2><%= pluralize(client.errors.count, "error") %> prohibited this client from being saved:</h2>
<ul>
<% client.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= form.label :company_name, style: "display: block" %>
<%= form.text_field :company_name %>
</div>
<div>
<%= form.label :firstname, style: "display: block" %>
<%= form.text_field :firstname %>
</div>
<div>
<%= form.label :lastname, style: "display: block" %>
<%= form.text_field :lastname %>
</div>
<div>
<%= form.label :streetname, style: "display: block" %>
<%= form.text_field :streetname %>
</div>
<div>
<%= form.label :zipcode, style: "display: block" %>
<%= form.text_field :zipcode %>
</div>
<div>
<%= form.label :city, style: "display: block" %>
<%= form.text_field :city %>
</div>
<div>
<%= form.label :country, style: "display: block" %>
<%= form.text_field :country %>
</div>
<div>
<%= form.label :email, style: "display: block" %>
<%= form.text_field :email %>
</div>
<div>
<%= form.label :phone, style: "display: block" %>
<%= form.text_field :phone %>
</div>
<div>
<%= form.submit %>
</div>
<% end %>

View File

@@ -0,0 +1,12 @@
<% content_for :title, "Editing client" %>
<h1>Editing client</h1>
<%= render "form", client: @client %>
<br>
<div>
<%= link_to "Show this client", @client %> |
<%= link_to "Back to clients", clients_path %>
</div>

View File

@@ -0,0 +1,16 @@
<p style="color: green"><%= notice %></p>
<% content_for :title, "Clients" %>
<h1>Clients</h1>
<div id="clients">
<% @clients.each do |client| %>
<%= render client %>
<p>
<%= link_to "Show this client", client %>
</p>
<% end %>
</div>
<%= link_to "New client", new_client_path %>

View File

@@ -0,0 +1 @@
json.array! @clients, partial: "clients/client", as: :client

View File

@@ -0,0 +1,11 @@
<% content_for :title, "New client" %>
<h1>New client</h1>
<%= render "form", client: @client %>
<br>
<div>
<%= link_to "Back to clients", clients_path %>
</div>

View File

@@ -0,0 +1,10 @@
<p style="color: green"><%= notice %></p>
<%= render @client %>
<div>
<%= link_to "Edit this client", edit_client_path(@client) %> |
<%= link_to "Back to clients", clients_path %>
<%= button_to "Destroy this client", @client, method: :delete %>
</div>

View File

@@ -0,0 +1 @@
json.partial! "clients/client", client: @client

View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<title><%= content_for(:title) || "Parse" %></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<%= 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) %>
<link rel="icon" href="/icon.png" type="image/png">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/icon.png">
<%# Includes all stylesheet files in app/assets/stylesheets %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
</head>
<body>
<%= yield %>
</body>
</html>

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style>
/* Email styles need to be inline */
</style>
</head>
<body>
<%= yield %>
</body>
</html>

View File

@@ -0,0 +1 @@
<%= yield %>

View File

@@ -0,0 +1,52 @@
<%= form_with(model: project) do |form| %>
<% if project.errors.any? %>
<div style="color: red">
<h2><%= pluralize(project.errors.count, "error") %> prohibited this project from being saved:</h2>
<ul>
<% project.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= form.label :name, style: "display: block" %>
<%= form.text_field :name %>
</div>
<h3>Subprojects</h3>
<%= form.fields_for :subprojects do |sp_form| %>
<div class="subproject-fields">
<%= sp_form.label :subproject_name %>
<%= sp_form.text_field :subproject_name %>
<% [:client, :owner, :builder].each do |role| %>
<div>
<%= sp_form.label "#{role}_id", "Select Existing #{role.capitalize}" %>
<%= sp_form.collection_select "#{role}_id", Client.all, :id, :company_name, prompt: "Select #{role.capitalize}" %>
<fieldset>
<legend>Or Create New <%= role.capitalize %></legend>
<%= sp_form.fields_for role do |c_form| %>
<%= c_form.label :company_name %>
<%= c_form.text_field :company_name %><br>
<%= c_form.label :firstname %>
<%= c_form.text_field :firstname %><br>
<%= c_form.label :lastname %>
<%= c_form.text_field :lastname %><br>
<%= c_form.label :email %>
<%= c_form.text_field :email %><br>
<!-- Add more client fields here as needed -->
<% end %>
</fieldset>
</div>
<% end %>
</div>
<% end %>
<div>
<%= form.submit %>
</div>
<% end %>

View File

@@ -0,0 +1,7 @@
<div id="<%= dom_id project %>">
<p>
<strong>Name:</strong>
<%= project.name %>
</p>
</div>

View File

@@ -0,0 +1,2 @@
json.extract! project, :id, :name, :created_at, :updated_at
json.url project_url(project, format: :json)

View File

@@ -0,0 +1,12 @@
<% content_for :title, "Editing project" %>
<h1>Editing project</h1>
<%= render "form", project: @project %>
<br>
<div>
<%= link_to "Show this project", @project %> |
<%= link_to "Back to projects", projects_path %>
</div>

View File

@@ -0,0 +1,16 @@
<p style="color: green"><%= notice %></p>
<% content_for :title, "Projects" %>
<h1>Projects</h1>
<div id="projects">
<% @projects.each do |project| %>
<%= render project %>
<p>
<%= link_to "Show this project", project %>
</p>
<% end %>
</div>
<%= link_to "New project", new_project_path %>

View File

@@ -0,0 +1 @@
json.array! @projects, partial: "projects/project", as: :project

View File

@@ -0,0 +1,11 @@
<% content_for :title, "New project" %>
<h1>New project</h1>
<%= render "form", project: @project %>
<br>
<div>
<%= link_to "Back to projects", projects_path %>
</div>

View File

@@ -0,0 +1,10 @@
<p style="color: green"><%= notice %></p>
<%= render @project %>
<div>
<%= link_to "Edit this project", edit_project_path(@project) %> |
<%= link_to "Back to projects", projects_path %>
<%= button_to "Destroy this project", @project, method: :delete %>
</div>

View File

@@ -0,0 +1 @@
json.partial! "projects/project", project: @project

View File

@@ -0,0 +1,22 @@
{
"name": "Parse",
"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": "Parse.",
"theme_color": "red",
"background_color": "red"
}

View File

@@ -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)
// }
// })
// )
// })

View File

@@ -0,0 +1,42 @@
<%= form_with(model: subproject) do |form| %>
<% if subproject.errors.any? %>
<div style="color: red">
<h2><%= pluralize(subproject.errors.count, "error") %> prohibited this subproject from being saved:</h2>
<ul>
<% subproject.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= form.label :subproject_name, style: "display: block" %>
<%= form.text_field :subproject_name %>
</div>
<div>
<%= form.label :project_id, style: "display: block" %>
<%= form.text_field :project_id %>
</div>
<div>
<%= form.label :client_id, style: "display: block" %>
<%= form.text_field :client_id %>
</div>
<div>
<%= form.label :owner_id, style: "display: block" %>
<%= form.text_field :owner_id %>
</div>
<div>
<%= form.label :builder_id, style: "display: block" %>
<%= form.text_field :builder_id %>
</div>
<div>
<%= form.submit %>
</div>
<% end %>

View File

@@ -0,0 +1,27 @@
<div id="<%= dom_id subproject %>">
<p>
<strong>Subproject name:</strong>
<%= subproject.subproject_name %>
</p>
<p>
<strong>Project:</strong>
<%= subproject.project_id %>
</p>
<p>
<strong>Client:</strong>
<%= subproject.client_id %>
</p>
<p>
<strong>Owner:</strong>
<%= subproject.owner_id %>
</p>
<p>
<strong>Builder:</strong>
<%= subproject.builder_id %>
</p>
</div>

View File

@@ -0,0 +1,2 @@
json.extract! subproject, :id, :subproject_name, :project_id, :client_id, :owner_id, :builder_id, :created_at, :updated_at
json.url subproject_url(subproject, format: :json)

View File

@@ -0,0 +1,12 @@
<% content_for :title, "Editing subproject" %>
<h1>Editing subproject</h1>
<%= render "form", subproject: @subproject %>
<br>
<div>
<%= link_to "Show this subproject", @subproject %> |
<%= link_to "Back to subprojects", subprojects_path %>
</div>

View File

@@ -0,0 +1,16 @@
<p style="color: green"><%= notice %></p>
<% content_for :title, "Subprojects" %>
<h1>Subprojects</h1>
<div id="subprojects">
<% @subprojects.each do |subproject| %>
<%= render subproject %>
<p>
<%= link_to "Show this subproject", subproject %>
</p>
<% end %>
</div>
<%= link_to "New subproject", new_subproject_path %>

View File

@@ -0,0 +1 @@
json.array! @subprojects, partial: "subprojects/subproject", as: :subproject

View File

@@ -0,0 +1,11 @@
<% content_for :title, "New subproject" %>
<h1>New subproject</h1>
<%= render "form", subproject: @subproject %>
<br>
<div>
<%= link_to "Back to subprojects", subprojects_path %>
</div>

View File

@@ -0,0 +1,10 @@
<p style="color: green"><%= notice %></p>
<%= render @subproject %>
<div>
<%= link_to "Edit this subproject", edit_subproject_path(@subproject) %> |
<%= link_to "Back to subprojects", subprojects_path %>
<%= button_to "Destroy this subproject", @subproject, method: :delete %>
</div>

View File

@@ -0,0 +1 @@
json.partial! "subprojects/subproject", subproject: @subproject