Merge branch 'master' into label_printing_on_A4

This commit is contained in:
d-buchmann 2025-11-07 13:46:40 +01:00 committed by GitHub
commit 248bc82eb5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
521 changed files with 59933 additions and 17536 deletions

View file

@ -1,4 +1,5 @@
{% import "helper.twig" as helper %}
{% import "vars.macro.twig" as vars %}
{% import "components/search.macro.html.twig" as search %}
<nav class="navbar navbar-expand-md bg-body-tertiary border-bottom shadow-sm fixed-top py-0" id="navbar">
@ -17,7 +18,7 @@
</div>
<a class="navbar-brand" href="{{ path('homepage') }}"><i class="fa fa-microchip"
aria-hidden="true"></i> {{ partdb_title }}</a>
aria-hidden="true"></i> {{ vars.partdb_title() }}</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent"
aria-controls="navbarContent" aria-expanded="false" aria-label="Toggle navigation">

View file

@ -2,8 +2,8 @@
<div class="nav flex-column">
{% for item in sidebar_items %}
{{ tree.treeview_sidebar('sidebar-panel-' ~ loop.index, item) }}
{% for item in settings_instance('sidebar').items %}
{{ tree.treeview_sidebar('sidebar-panel-' ~ loop.index, item.value) }}
<div class="mb-2"></div>
{% endfor %}
</div>

View file

@ -22,9 +22,14 @@
<div class="d-none" data-title="{{ current_page_title|trim|raw }}" {{ stimulus_controller('turbo/title') }}></div>
<div class="d-none" {{ stimulus_controller('turbo/locale_menu') }}>
{% for locale in locale_menu %}
{% set locales = settings_instance('localization').languageMenuEntries %}
{% if locales is empty %}
{% set locales = locale_menu %}
{% endif %}
{% for locale in locales %}
<a class="dropdown-item" data-turbo="false" data-turbo-frame="_top" href="{{ path(app.request.attributes.get('_route'),
app.request.query.all|merge(app.request.attributes.get('_route_params'))|merge({'_locale': locale})) }}">
{{ locale|language_name }} ({{ locale|upper }})</a>
{% endfor %}
</div>
</div>

View file

@ -86,7 +86,7 @@
<li class="nav-item">
<a data-bs-toggle="tab" class="nav-link link-anchor" href="#attachments">{% trans %}admin.attachments{% endtrans %}</a>
</li>
{% if entity.parameters is defined %}
{% if entity.parameters is defined and showParameters == true %}
<li class="nav-item">
<a data-bs-toggle="tab" class="nav-link link-anchor" href="#parameters">{% trans %}admin.parameters{% endtrans %}</a>
</li>

View file

@ -31,6 +31,7 @@
<hr>
{{ form_row(form.partname_regex) }}
{{ form_row(form.partname_hint) }}
{{ form_row(form.part_ipn_prefix) }}
<hr>
{{ form_row(form.default_description) }}
{{ form_row(form.default_comment) }}

View file

@ -1,5 +1,7 @@
{% extends "admin/base_admin.html.twig" %}
{% import "vars.macro.twig" as vars %}
{% block card_title %}
<i class="fa-solid fa-coins"></i> {% trans %}currency.caption{% endtrans %}
{% endblock %}
@ -20,8 +22,8 @@
{{ form_row(form.exchange_rate) }}
{% if entity.inverseExchangeRate %}
<p class="form-text text-muted offset-3 col-9">
{{ '1'|format_currency(default_currency) }} = {{ entity.inverseExchangeRate.tofloat | format_currency(entity.isoCode, {fraction_digit: 5}) }}<br>
{{ '1'|format_currency(entity.isoCode) }} = {{ entity.exchangeRate.tofloat | format_currency(default_currency, {fraction_digit: 5}) }}
{{ '1'|format_currency(vars.base_currency()) }} = {{ entity.inverseExchangeRate.tofloat | format_currency(entity.isoCode, {fraction_digit: 5}) }}<br>
{{ '1'|format_currency(entity.isoCode) }} = {{ entity.exchangeRate.tofloat | format_currency(vars.base_currency(), {fraction_digit: 5}) }}
</p>
{% endif %}

View file

@ -0,0 +1,14 @@
{% extends "admin/base_admin.html.twig" %}
{% block card_title %}
<i class="fas fa-balance-scale fa-tools"></i> {% trans %}part_custom_state.caption{% endtrans %}
{% endblock %}
{% block edit_title %}
{% trans %}part_custom_state.edit{% endtrans %}: {{ entity.name }}
{% endblock %}
{% block new_title %}
{% trans %}part_custom_state.new{% endtrans %}
{% endblock %}

View file

@ -1,3 +1,5 @@
{% import "vars.macro.twig" as vars %}
<!DOCTYPE html>
<html lang="{{ app.request.locale | replace({"_": "-"}) }}"
{# For the UX translator, just use the language part (before the _. should be 2 chars), otherwise it finds no translations #}
@ -31,21 +33,19 @@
<link rel="mask-icon" href="{{ asset('icon/safari-pinned-tab.svg') }}" color="#5bbad5">
{# The content block is already escaped. so we must not escape it again. #}
<title>{% apply trim|raw %}{% block title %}{{ partdb_title }}{% endblock %}{% endapply %}</title>
<title>{% apply trim|raw %}{% block title %}{{ vars.partdb_title() }}{% endblock %}{% endapply %}</title>
{% set current_page_title = block("title")|raw %}
{% block stylesheets %}
{# Include the main bootstrap theme based on user/global setting #}
{% if not app.user.theme is defined or app.user.theme is null %}
{% set theme = global_theme %}
{% if app.user.theme is not defined or app.user.theme is null %}
{% set theme = settings_instance('customization').theme %}
{% else %}
{% set theme = app.user.theme %}
{% endif %}
{% if theme and theme in available_themes and encore_entry_exists('theme_' ~ theme) %}
{{ encore_entry_link_tags('theme_' ~ theme) }}
{% else %}
@ -53,17 +53,19 @@
{% endif %}
{{ encore_entry_link_tags('app') }}
{% set table_settings = settings_instance('table') %}
<style>
:root {
--table-image-preview-min-size: {{ table_settings.previewImageMinWidth }}px;
--table-image-preview-max-size: {{ table_settings.previewImageMaxWidth }}px;
}
</style>
{% endblock %}
{% block javascripts %}
{{ encore_entry_script_tags('app') }}
{{ encore_entry_script_tags('webauthn_tfa') }}
{# load translation files for ckeditor #}
{% set two_chars_locale = app.request.locale|default("en")|slice(0,2) %}
{% if two_chars_locale != "en" %}
<script src="{{ asset("build/ckeditor_translations/" ~ two_chars_locale ~ ".js") }}"></script>
{% endif %}
{% endblock %}
</head>
<body data-base-url="{{ path('homepage', {'_locale': app.request.locale}) }}" data-locale="{{ app.request.locale|default("en")|slice(0,2) }}">

View file

@ -1,6 +1,9 @@
{% extends "bundles/TwigBundle/Exception/error.html.twig" %}
{% block status_comment %}
Nice try! But you are not allowed to do this!
Nice try! But you are not allowed to do this!<br>
<code>{{ exception.message }}</code>
<br> <small>If you think you should have access to this ressource, contact the adminstrator.</small>
{% endblock %}
{% endblock %}

View file

@ -29,9 +29,7 @@
<input type="hidden" name="ids" {{ stimulus_target('elements/datatables/parts', 'selectIDs') }} value="">
<div class="d-none mb-2" {{ stimulus_target('elements/datatables/parts', 'selectPanel') }}>
{# <span id="select_count"></span> #}
<div class="d-none mb-2 bg-body-tertiary shadow-sm border border-secondary rounded mx-2 p-2" {{ stimulus_target('elements/datatables/parts', 'selectPanel') }}>
<div class="input-group">
<button class="btn btn-outline-secondary" type="button" {{ stimulus_action('elements/datatables/parts', 'invertSelection')}}
title="{% trans %}part_list.action.invert_selection{% endtrans %}" ><i class="fa-solid fa-arrow-right-arrow-left"></i></button>
@ -41,7 +39,7 @@
<select class="form-select" name="action" data-controller="elements--select" {{ stimulus_action('elements/datatables/parts', 'updateTargetPicker', 'change') }}
title="{% trans %}part_list.action.action.title{% endtrans %}" required>
<optgroup label="{% trans %}part_list.action.action.group.favorite{% endtrans %}">
<optgroup label="{% trans %}part_list.action.action.group.favorite{% endtrans %} ({% trans %}part_list.action.scrollable_hint{% endtrans %})">
<option {% if not is_granted('@parts.change_favorite') %}disabled{% endif %} value="favorite">{% trans %}part_list.action.action.favorite{% endtrans %}</option>
<option {% if not is_granted('@parts.change_favorite') %}disabled{% endif %} value="unfavorite">{% trans %}part_list.action.action.unfavorite{% endtrans %}</option>
</optgroup>
@ -72,6 +70,10 @@
<option {% if not is_granted('@parts.read') %}disabled{% endif %} value="export_csv" data-url="{{ path('select_export_level')}}" data-turbo="false">{% trans %}part_list.action.export_csv{% endtrans %}</option>
<option {% if not is_granted('@parts.read') %}disabled{% endif %} value="export_yaml" data-url="{{ path('select_export_level')}}" data-turbo="false">{% trans %}part_list.action.export_yaml{% endtrans %}</option>
<option {% if not is_granted('@parts.read') %}disabled{% endif %} value="export_xml" data-url="{{ path('select_export_level')}}" data-turbo="false">{% trans %}part_list.action.export_xml{% endtrans %}</option>
<option {% if not is_granted('@parts.read') %}disabled{% endif %} value="export_xlsx" data-url="{{ path('select_export_level')}}" data-turbo="false">{% trans %}part_list.action.export_xlsx{% endtrans %}</option>
</optgroup>
<optgroup label="{% trans %}part_list.action.action.info_provider{% endtrans %}">
<option {% if not is_granted('@info_providers.create_parts') %}disabled{% endif %} value="bulk_info_provider_import" data-url="{{ path('bulk_info_provider_step1')}}" data-turbo="false">{% trans %}part_list.action.bulk_info_provider_import{% endtrans %}</option>
</optgroup>
</select>
@ -95,4 +97,4 @@
</div>
</form>
{% endmacro %}
{% endmacro %}

View file

@ -6,7 +6,7 @@
['footprints', path('tree_footprint_root'), 'footprint.labelp', is_granted('@footprints.read') and is_granted('@parts.read')],
['manufacturers', path('tree_manufacturer_root'), 'manufacturer.labelp', is_granted('@manufacturers.read') and is_granted('@parts.read')],
['suppliers', path('tree_supplier_root'), 'supplier.labelp', is_granted('@suppliers.read') and is_granted('@parts.read')],
['devices', path('tree_device_root'), 'project.labelp', is_granted('@projects.read')],
['projects', path('tree_device_root'), 'project.labelp', is_granted('@projects.read')],
['tools', path('tree_tools'), 'tools.label', true],
] %}

View file

@ -1,5 +1,9 @@
{% extends 'bootstrap_5_horizontal_layout.html.twig' %}
{%- block toggle_password_widget -%}
<div class="{{ toggle_container_classes|join(' ') }}">{{ block('password_widget') }}</div>
{%- endblock toggle_password_widget -%}
{# Make form rows smaller #}
{% block form_row -%}
{%- set row_attr = row_attr|merge({"class": "mb-2"}) -%}
@ -139,4 +143,4 @@
{% else %}
{{- parent() -}}
{% endif %}
{% endblock %}
{% endblock %}

View file

@ -70,18 +70,20 @@
{% endif %}
{% if show_presets %}
{# This hidden field is there to ensure that none of the presets is submitted, if a user presses enter #}
<input type="submit" name="group_admin_form[save]" class="d-none">
<div class="col text-end">
<div class="btn-group">
<button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
{% trans %}permission.preset.button{% endtrans %}
</button>
<ul class="dropdown-menu">
<li><button type="submit" name="permission_preset" value="read_only" class="dropdown-item" >{% trans %}permission.preset.read_only{% endtrans%} <br><small class="text-muted">{% trans %}permission.preset.read_only.desc{% endtrans%}</small></button></li>
<li><button type="submit" name="permission_preset" value="editor" class="dropdown-item" >{% trans %}permission.preset.editor{% endtrans%} <br><small class="text-muted">{% trans %}permission.preset.editor.desc{% endtrans%}</small></button></li>
<li><button type="submit" name="permission_preset" value="admin" class="dropdown-item" >{% trans %}permission.preset.admin{% endtrans%} <br><small class="text-muted">{% trans %}permission.preset.admin.desc{% endtrans%}</small></button></li>
<li><button type="submit" name="permission_preset" value="read_only" class="dropdown-item">{% trans %}permission.preset.read_only{% endtrans%} <br><small class="text-muted">{% trans %}permission.preset.read_only.desc{% endtrans%}</small></button></li>
<li><button type="submit" name="permission_preset" value="editor" class="dropdown-item">{% trans %}permission.preset.editor{% endtrans%} <br><small class="text-muted">{% trans %}permission.preset.editor.desc{% endtrans%}</small></button></li>
<li><button type="submit" name="permission_preset" value="admin" class="dropdown-item">{% trans %}permission.preset.admin{% endtrans%} <br><small class="text-muted">{% trans %}permission.preset.admin.desc{% endtrans%}</small></button></li>
<li><hr class="dropdown-divider"></li>
<li><button type="submit" name="permission_preset" value="all_inherit" class="dropdown-item" >{% trans %}permission.preset.all_inherit{% endtrans%}<br><small class="text-muted">{% trans %}permission.preset.all_inherit.desc{% endtrans%}</small></button></li>
<li><button type="submit" name="permission_preset" value="all_forbid" class="dropdown-item" >{% trans %}permission.preset.all_forbid{% endtrans%}<br><small class="text-muted">{% trans %}permission.preset.all_forbid.desc{% endtrans%}</small></button></li>
<li><button type="submit" name="permission_preset" value="all_inherit" class="dropdown-item">{% trans %}permission.preset.all_inherit{% endtrans%}<br><small class="text-muted">{% trans %}permission.preset.all_inherit.desc{% endtrans%}</small></button></li>
<li><button type="submit" name="permission_preset" value="all_forbid" class="dropdown-item">{% trans %}permission.preset.all_forbid{% endtrans%}<br><small class="text-muted">{% trans %}permission.preset.all_forbid.desc{% endtrans%}</small></button></li>
<li><button type="submit" name="permission_preset" value="all_allow" class="dropdown-item" >{% trans %}permission.preset.all_allow{% endtrans%}<br><small class="text-muted">{% trans %}permission.preset.all_allow.desc{% endtrans%}</small></button></li>
</ul>
</div>
@ -110,4 +112,4 @@
{% endfor %}
</div>
{% endblock %}
{% endblock %}

View file

@ -0,0 +1,25 @@
{% extends "form/extended_bootstrap_layout.html.twig" %}
{% block form_label %}
{# If parameter_envvar exists on form then show it as tooltip #}
{% if parameter_envvar is defined and parameter_envvar is not null %}
{%- set label_attr = label_attr|merge({title: 'settings.tooltip.overrideable_by_env'|trans(arguments = {'%env%': (parameter_envvar)|trim})}) -%}
{% endif %}
{{- parent() -}}
{% endblock %}
{% block checkbox_radio_label %}
{# If parameter_envvar exists on form then show it as tooltip #}
{% if parameter_envvar is defined and parameter_envvar is not null %}
{%- set label_attr = label_attr|merge({title: 'settings.tooltip.overrideable_by_env'|trans(arguments = {'%env%': (parameter_envvar)|trim})}) -%}
{% endif %}
{{- parent() -}}
{% endblock %}
{% block tristate_label %}
{# If parameter_envvar exists on form then show it as tooltip #}
{% if parameter_envvar is defined and parameter_envvar is not null %}
{%- set label_attr = label_attr|merge({title: 'settings.tooltip.overrideable_by_env'|trans(arguments = {'%env%': (parameter_envvar)|trim})}) -%}
{% endif %}
{{- parent() -}}
{% endblock %}

View file

@ -214,11 +214,11 @@
{% endmacro %}
{% macro parameters_table(parameters) %}
<table class="table table-hover table-striped table-sm">
<table class="table table-hover table-striped table-sm" style="table-layout: fixed;">
<thead>
<tr>
<th>{% trans %}specifications.property{% endtrans %}</th>
<th>{% trans %}specifications.symbol{% endtrans %}</th>
<th class="col-sm-1">{% trans %}specifications.symbol{% endtrans %}</th>
<th>{% trans %}specifications.value{% endtrans %}</th>
</tr>
</thead>
@ -240,4 +240,4 @@
{% else %}
{{ datetime|format_datetime }}
{% endif %}
{% endmacro %}
{% endmacro %}

View file

@ -2,27 +2,25 @@
{% import "components/new_version.macro.html.twig" as nv %}
{% import "components/search.macro.html.twig" as search %}
{% import "vars.macro.twig" as vars %}
{% block content %}
{% if is_granted('@system.show_updates') %}
{{ nv.new_version_alert(new_version_available, new_version, new_version_url) }}
{% endif %}
{% block item_search %}
{% if is_granted('@parts.read') %}
{{ search.search_form("standalone") }}
<div class="mb-2"></div>
{% endif %}
{% endblock %}
{% block item_banner %}
<div class="rounded p-4 bg-body-secondary">
<h1 class="display-3">{{ partdb_title }}</h1>
<h4>
{% trans %}version.caption{% endtrans %}: {{ shivas_app_version }}
{% if git_branch is not empty or git_commit is not empty %}
({{ git_branch ?? '' }}/{{ git_commit ?? '' }})
{% endif %}
</h4>
<h1 class="display-3">{{ vars.partdb_title() }}</h1>
{% if settings_instance('customization').showVersionOnHomepage %}
<h4>
{% trans %}version.caption{% endtrans %}: {{ shivas_app_version }}
{% if git_branch is not empty or git_commit is not empty %}
({{ git_branch ?? '' }}/{{ git_commit ?? '' }})
{% endif %}
</h4>
{% endif %}
{% if banner is not empty %}
<hr>
<div class="latex" data-controller="common--latex">
@ -30,9 +28,11 @@
</div>
{% endif %}
</div>
{% endblock %}
{% block item_first_steps %}
{% if show_first_steps %}
<div class="card border-info mt-3">
<div class="card border-info">
<div class="card-header bg-info ">
<h4><i class="fa fa-circle-play fa-fw " aria-hidden="true"></i> {% trans %}homepage.first_steps.title{% endtrans %}</h4>
</div>
@ -50,8 +50,10 @@
</div>
</div>
{% endif %}
{% endblock %}
<div class="card border-primary mt-3">
{% block item_license %}
<div class="card border-primary">
<div class="card-header bg-primary text-white">
<h4><i class="fa fa-book fa-fw" aria-hidden="true"></i> {% trans %}homepage.license{% endtrans %}</h4>
</div>
@ -67,9 +69,11 @@
<strong><i class="fas fa-comments fa-fw"></i> {% trans %}homepage.forum.caption{% endtrans %}:</strong> {% trans with {'%href%': 'https://github.com/Part-DB/Part-DB-server/discussions'}%}homepage.forum.text{% endtrans %}<br>
</div>
</div>
{% endblock %}
{% block item_last_activity %}
{% if datatable is not null %}
<div class="card mt-3">
<div class="card">
<div class="card-header"><i class="fas fa-fw fa-history"></i> {% trans %}homepage.last_activity{% endtrans %}</div>
<div class="card-body">
{% import "components/history_log_macros.html.twig" as log %}
@ -77,4 +81,23 @@
</div>
</div>
{% endif %}
{% endblock %}
{% endblock %}
{% block content %}
{% if is_granted('@system.show_updates') %}
{{ nv.new_version_alert(new_version_available, new_version, new_version_url) }}
{% endif %}
{% for item in settings_instance('customization').homepageitems %}
{% if block('item_' ~ item.value) is defined %}
{{ block('item_' ~ item.value) }}
<div class="mb-2"></div>
{% else %}
<div class="alert alert-warning mt-3" role="alert">
Alert: The homepage item "{{ item.value }}" is not defined!
</div>
{% endif %}
{% endfor %}
{% endblock %}

View file

@ -0,0 +1,124 @@
{% extends "main_card.html.twig" %}
{% block title %}
{% trans %}info_providers.bulk_import.manage_jobs{% endtrans %}
{% endblock %}
{% block card_title %}
<i class="fas fa-tasks"></i> {% trans %}info_providers.bulk_import.manage_jobs{% endtrans %}
{% endblock %}
{% block card_content %}
<div data-controller="bulk-job-manage"
data-bulk-job-manage-delete-url-value="{{ path('bulk_info_provider_delete', {'jobId': '__JOB_ID__'}) }}"
data-bulk-job-manage-stop-url-value="{{ path('bulk_info_provider_stop', {'jobId': '__JOB_ID__'}) }}"
data-bulk-job-manage-delete-confirm-message-value="{% trans %}info_providers.bulk_import.confirm_delete_job{% endtrans %}"
data-bulk-job-manage-stop-confirm-message-value="{% trans %}info_providers.bulk_import.confirm_stop_job{% endtrans %}">
<div class="d-flex justify-content-between align-items-center mb-3">
<p class="text-muted mb-0">
{% trans %}info_providers.bulk_import.manage_jobs_description{% endtrans %}
</p>
</div>
{% if jobs is not empty %}
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>{% trans %}info_providers.bulk_import.job_name{% endtrans %}</th>
<th>{% trans %}info_providers.bulk_import.parts_count{% endtrans %}</th>
<th>{% trans %}info_providers.bulk_import.results_count{% endtrans %}</th>
<th>{% trans %}info_providers.bulk_import.progress{% endtrans %}</th>
<th>{% trans %}info_providers.bulk_import.status{% endtrans %}</th>
<th>{% trans %}info_providers.bulk_import.created_by{% endtrans %}</th>
<th>{% trans %}info_providers.bulk_import.created_at{% endtrans %}</th>
<th>{% trans %}info_providers.bulk_import.completed_at{% endtrans %}</th>
<th>{% trans %}info_providers.bulk_import.action.label{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for job in jobs %}
<tr>
<td>
<strong>{{ job.displayNameKey|trans(job.displayNameParams) }} - {{ job.formattedTimestamp }}</strong>
{% if job.isInProgress %}
<span class="badge bg-info ms-2">Active</span>
{% endif %}
</td>
<td>{{ job.partCount }}</td>
<td>{{ job.resultCount }}</td>
<td>
<div class="d-flex align-items-center">
<div class="progress me-2" style="width: 80px; height: 12px;">
<div class="progress-bar {% if job.isCompleted %}bg-success{% elseif job.isFailed %}bg-danger{% else %}bg-info{% endif %}"
role="progressbar"
style="width: {{ job.progressPercentage }}%"
aria-valuenow="{{ job.progressPercentage }}"
aria-valuemin="0" aria-valuemax="100">
</div>
</div>
<small class="text-muted">{{ job.progressPercentage }}%</small>
</div>
<small class="text-muted">
{% trans with {'%current%': job.completedPartsCount + job.skippedPartsCount, '%total%': job.partCount} %}info_providers.bulk_import.progress_label{% endtrans %}
</small>
</td>
<td>
{% if job.isPending %}
<span class="badge bg-warning">{% trans %}info_providers.bulk_import.status.pending{% endtrans %}</span>
{% elseif job.isInProgress %}
<span class="badge bg-info">{% trans %}info_providers.bulk_import.status.in_progress{% endtrans %}</span>
{% elseif job.isCompleted %}
<span class="badge bg-success">{% trans %}info_providers.bulk_import.status.completed{% endtrans %}</span>
{% elseif job.isStopped %}
<span class="badge bg-secondary">{% trans %}info_providers.bulk_import.status.stopped{% endtrans %}</span>
{% elseif job.isFailed %}
<span class="badge bg-danger">{% trans %}info_providers.bulk_import.status.failed{% endtrans %}</span>
{% endif %}
</td>
<td>{{ job.createdBy.fullName(true) }}</td>
<td>{{ job.createdAt|format_datetime('short') }}</td>
<td>
{% if job.completedAt %}
{{ job.completedAt|format_datetime('short') }}
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
{% if job.isInProgress or job.isCompleted or job.isStopped %}
<a href="{{ path('bulk_info_provider_step2', {'jobId': job.id}) }}" class="btn btn-primary">
<i class="fas fa-eye"></i> {% trans %}info_providers.bulk_import.view_results{% endtrans %}
</a>
{% endif %}
{% if job.canBeStopped %}
<button type="button" class="btn btn-warning" data-action="click->bulk-job-manage#stopJob" data-job-id="{{ job.id }}">
<i class="fas fa-stop"></i> {% trans %}info_providers.bulk_import.action.stop{% endtrans %}
</button>
{% endif %}
{% if job.isCompleted or job.isFailed or job.isStopped %}
<button type="button" class="btn btn-danger" data-action="click->bulk-job-manage#deleteJob" data-job-id="{{ job.id }}">
<i class="fas fa-trash"></i> {% trans %}info_providers.bulk_import.action.delete{% endtrans %}
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info" role="alert">
<i class="fas fa-info-circle"></i>
{% trans %}info_providers.bulk_import.no_jobs_found{% endtrans %}<br>
{% trans %}info_providers.bulk_import.create_first_job{% endtrans %}
</div>
{% endif %}
</div>
{% endblock %}

View file

@ -0,0 +1,304 @@
{% extends "main_card.html.twig" %}
{% import "info_providers/providers.macro.html.twig" as providers_macro %}
{% import "helper.twig" as helper %}
{% block title %}
{% trans %}info_providers.bulk_import.step1.title{% endtrans %}
{% endblock %}
{% block card_title %}
<i class="fas fa-cloud-arrow-down"></i> {% trans %}info_providers.bulk_import.step1.title{% endtrans %}
<span class="badge bg-secondary">{{ parts|length }} {% trans %}info_providers.bulk_import.parts_selected{% endtrans %}</span>
{% endblock %}
{% block card_content %}
<div>
<!-- Show existing jobs -->
{% if existing_jobs is not empty %}
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">{% trans %}info_providers.bulk_import.existing_jobs{% endtrans %}</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>{% trans %}info_providers.bulk_import.job_name{% endtrans %}</th>
<th>{% trans %}info_providers.bulk_import.parts_count{% endtrans %}</th>
<th>{% trans %}info_providers.bulk_import.results_count{% endtrans %}</th>
<th>{% trans %}info_providers.bulk_import.progress{% endtrans %}</th>
<th>{% trans %}info_providers.bulk_import.status{% endtrans %}</th>
<th>{% trans %}info_providers.bulk_import.created_at{% endtrans %}</th>
<th>{% trans %}info_providers.bulk_import.action.label{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for job in existing_jobs %}
<tr>
<td>{{ job.displayNameKey|trans(job.displayNameParams) }} - {{ job.formattedTimestamp }}</td>
<td>{{ job.partCount }}</td>
<td>{{ job.resultCount }}</td>
<td>
<div class="d-flex align-items-center">
<div class="progress me-2" style="width: 60px; height: 8px;">
<div class="progress-bar {% if job.isCompleted %}bg-success{% else %}bg-info{% endif %}"
role="progressbar"
style="width: {{ job.progressPercentage }}%"
aria-valuenow="{{ job.progressPercentage }}"
aria-valuemin="0" aria-valuemax="100">
</div>
</div>
<small class="text-muted">{{ job.progressPercentage }}%</small>
</div>
<small class="text-muted">{{ job.completedPartsCount }}/{{ job.partCount }}</small>
</td>
<td>
{% if job.isPending %}
<span class="badge bg-warning">{% trans %}info_providers.bulk_import.status.pending{% endtrans %}</span>
{% elseif job.isInProgress %}
<span class="badge bg-info">{% trans %}info_providers.bulk_import.status.in_progress{% endtrans %}</span>
{% elseif job.isCompleted %}
<span class="badge bg-success">{% trans %}info_providers.bulk_import.status.completed{% endtrans %}</span>
{% elseif job.isFailed %}
<span class="badge bg-danger">{% trans %}info_providers.bulk_import.status.failed{% endtrans %}</span>
{% endif %}
</td>
<td>{{ job.createdAt|date('Y-m-d H:i') }}</td>
<td>
{% if job.isInProgress or job.isCompleted %}
<a href="{{ path('bulk_info_provider_step2', {'jobId': job.id}) }}" class="btn btn-primary btn-sm">
<i class="fas fa-eye"></i> {% trans %}info_providers.bulk_import.view_results{% endtrans %}
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
<div class="alert alert-info" role="alert">
<i class="fas fa-info-circle"></i>
{% trans %}info_providers.bulk_import.step1.global_mapping_description{% endtrans %}
</div>
<div class="alert alert-success" role="alert">
<i class="fas fa-lightbulb"></i>
<strong>{% trans %}info_providers.bulk_import.priority_system.title{% endtrans %}:</strong> {% trans %}info_providers.bulk_import.priority_system.description{% endtrans %}
<br><small class="text-muted">
{% trans %}info_providers.bulk_import.priority_system.example{% endtrans %}
</small>
</div>
<div class="alert alert-warning" role="alert">
<i class="fas fa-exclamation-triangle"></i>
{% trans %}info_providers.bulk_import.step1.spn_recommendation{% endtrans %}
</div>
<!-- Show selected parts -->
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">{% trans %}info_providers.bulk_import.selected_parts{% endtrans %}</h5>
</div>
<div class="card-body">
<div class="row">
{% for part in parts %}
{% set hasNoIdentifiers = part.manufacturerProductNumber is empty and part.orderdetails is empty %}
<div class="col-md-6 col-lg-4 mb-2">
<div class="d-flex align-items-center {% if hasNoIdentifiers %}text-danger{% endif %}">
<i class="fas fa-microchip {% if hasNoIdentifiers %}text-danger{% else %}text-primary{% endif %} me-2"></i>
<div>
<a href="{{ path('app_part_show', {'id': part.id}) }}" class="text-decoration-none {% if hasNoIdentifiers %}text-danger{% endif %}">
<strong>{{ part.name }}</strong>
{% if part.manufacturerProductNumber %}
<br><small class="{% if hasNoIdentifiers %}text-danger{% else %}text-muted{% endif %}">MPN: {{ part.manufacturerProductNumber }}</small>
{% endif %}
{% if part.orderdetails is not empty %}
<br><small class="{% if hasNoIdentifiers %}text-danger{% else %}text-muted{% endif %}">
SPNs: {{ part.orderdetails|map(od => od.supplierPartNr)|join(', ') }}
</small>
{% endif %}
</a>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{{ form_start(form) }}
<div class="card"
data-controller="field-mapping"
data-field-mapping-mapping-index-value="{{ form.field_mappings|length }}"
data-field-mapping-max-mappings-value="{{ fieldChoices|length }}"
data-field-mapping-prototype-value="{{ form_widget(form.field_mappings.vars.prototype)|e('html_attr') }}"
data-field-mapping-max-mappings-reached-message-value="{{ 'info_providers.bulk_import.max_mappings_reached'|trans|e('js') }}">
<div class="card-header">
<h5 class="mb-0">{% trans %}info_providers.bulk_import.field_mappings{% endtrans %}</h5>
<small class="text-muted">{% trans %}info_providers.bulk_import.field_mappings_help{% endtrans %}</small>
</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr>
<th>{% trans %}info_providers.bulk_search.search_field{% endtrans %}</th>
<th>{% trans %}info_providers.bulk_search.providers{% endtrans %}</th>
<th width="80">{% trans %}info_providers.bulk_search.priority{% endtrans %}</th>
<th width="100">{% trans %}info_providers.bulk_import.actions.label{% endtrans %}</th>
</tr>
</thead>
<tbody id="field-mappings-tbody" data-field-mapping-target="tbody">
{% for mapping in form.field_mappings %}
<tr class="mapping-row">
<td>{{ form_widget(mapping.field) }}{{ form_errors(mapping.field) }}</td>
<td>{{ form_widget(mapping.providers) }}{{ form_errors(mapping.providers) }}</td>
<td>{{ form_widget(mapping.priority) }}{{ form_errors(mapping.priority) }}</td>
<td>
<button type="button" class="btn btn-danger btn-sm" data-action="click->field-mapping#removeMapping">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<button type="button" class="btn btn-success btn-sm" id="addMappingBtn"
data-field-mapping-target="addButton"
data-action="click->field-mapping#addMapping">
<i class="fas fa-plus"></i> {% trans %}info_providers.bulk_import.add_mapping{% endtrans %}
</button>
</div>
</div>
<div class="mb-2 d-flex flex-column align-items-start gap-2">
<div class="mb-2">
<a href="{{ path('info_providers_list') }}">{% trans %}info_providers.search.info_providers_list{% endtrans %}</a>
|
<a href="{{ path('bulk_info_provider_manage') }}">{% trans %}info_providers.bulk_import.manage_jobs{% endtrans %}</a>
</div>
<div class="form-check mb-2">
{{ form_widget(form.prefetch_details, {'attr': {'class': 'form-check-input'}}) }}
{{ form_label(form.prefetch_details, null, {'label_attr': {'class': 'form-check-label'}}) }}
{{ form_help(form.prefetch_details) }}
</div>
{{ form_widget(form.submit, {'attr': {'class': 'btn btn-primary', 'data-field-mapping-target': 'submitButton'}}) }}
</div>
{{ form_end(form) }}
{% if search_results is not null %}
<hr>
<h4>{% trans %}info_providers.bulk_import.search_results.title{% endtrans %}</h4>
{% for part_result in search_results %}
{% set part = part_result.part %}
<div class="card mb-3">
<div class="card-header">
<h5 class="card-title mb-0">
{{ part.name }}
{% if part_result.errors is not empty %}
<span class="badge bg-warning">{{ part_result.errors|length }} {% trans %}info_providers.bulk_import.errors{% endtrans %}</span>
{% endif %}
<span class="badge bg-success">{{ part_result.search_results|length }} {% trans %}info_providers.bulk_import.results_found{% endtrans %}</span>
</h5>
</div>
<div class="card-body">
{% if part_result.errors is not empty %}
{% for error in part_result.errors %}
<div class="alert alert-warning" role="alert">
<i class="fas fa-exclamation-triangle"></i>
{{ error }}
</div>
{% endfor %}
{% endif %}
{% if part_result.search_results|length > 0 %}
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th></th>
<th>{% trans %}name.label{% endtrans %}</th>
<th>{% trans %}description.label{% endtrans %}</th>
<th>{% trans %}manufacturer.label{% endtrans %}</th>
<th>{% trans %}info_providers.table.provider.label{% endtrans %}</th>
<th>{% trans %}info_providers.bulk_import.source_field{% endtrans %}</th>
<th>{% trans %}info_providers.bulk_import.action.label{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for result in part_result.search_results %}
{% set dto = result.dto %}
{% set localPart = result.localPart %}
<tr {% if localPart is not null %}class="table-warning"{% endif %}>
<td>
<img src="{{ dto.preview_image_url }}" data-thumbnail="{{ dto.preview_image_url }}"
class="hoverpic" style="max-width: 30px;" {{ stimulus_controller('elements/hoverpic') }}>
</td>
<td>
{% if dto.provider_url is not null %}
<a href="{{ dto.provider_url }}" target="_blank" rel="noopener">{{ dto.name }}</a>
{% else %}
{{ dto.name }}
{% endif %}
{% if dto.mpn is not null %}
<br><small class="text-muted">{{ dto.mpn }}</small>
{% endif %}
</td>
<td>{{ dto.description }}</td>
<td>{{ dto.manufacturer ?? '' }}</td>
<td>
{{ info_provider_label(dto.provider_key)|default(dto.provider_key) }}
<br><small class="text-muted">{{ dto.provider_id }}</small>
</td>
<td>
<span class="badge bg-info">{{ result.source_field ?? 'unknown' }}</span>
{% if result.source_keyword %}
<br><small class="text-muted">{{ result.source_keyword }}</small>
{% endif %}
</td>
<td>
<div class="btn-group-vertical btn-group-sm" role="group">
{% set updateHref = path('info_providers_update_part',
{'id': part.id, 'providerKey': dto.provider_key, 'providerId': dto.provider_id}) %}
<a class="btn btn-primary" href="{{ updateHref }}" target="_blank">
<i class="fas fa-edit"></i> {% trans %}info_providers.bulk_import.update_part{% endtrans %}
</a>
{% if localPart is not null %}
<a class="btn btn-info btn-sm" href="{{ path('app_part_show', {'id': localPart.id}) }}" target="_blank">
<i class="fas fa-eye"></i> {% trans %}info_providers.bulk_import.view_existing{% endtrans %}
</a>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info" role="alert">
{% trans %}info_providers.search.no_results{% endtrans %}
</div>
{% endif %}
</div>
</div>
{% endfor %}
{% endif %}
</div>
{% endblock %}

View file

@ -0,0 +1,240 @@
{% extends "main_card.html.twig" %}
{% import "info_providers/providers.macro.html.twig" as providers_macro %}
{% import "helper.twig" as helper %}
{% block title %}
{% trans %}info_providers.bulk_import.step2.title{% endtrans %}
{% endblock %}
{% block card_title %}
<i class="fas fa-search"></i> {% trans %}info_providers.bulk_import.step2.title{% endtrans %}
<span class="badge bg-secondary">{{ job.displayNameKey|trans(job.displayNameParams) }} - {{ job.formattedTimestamp }}</span>
{% endblock %}
{% block card_content %}
<div {{ stimulus_controller('bulk-import', {
'jobId': job.id,
'researchUrl': path('bulk_info_provider_research_part', {'jobId': job.id, 'partId': '__PART_ID__'}),
'researchAllUrl': path('bulk_info_provider_research_all', {'jobId': job.id}),
'markCompletedUrl': path('bulk_info_provider_mark_completed', {'jobId': job.id, 'partId': '__PART_ID__'}),
'markSkippedUrl': path('bulk_info_provider_mark_skipped', {'jobId': job.id, 'partId': '__PART_ID__'}),
'markPendingUrl': path('bulk_info_provider_mark_pending', {'jobId': job.id, 'partId': '__PART_ID__'})
}) }}>
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h5 class="mb-1">{{ job.displayNameKey|trans(job.displayNameParams) }} - {{ job.formattedTimestamp }}</h5>
<small class="text-muted">
{{ job.partCount }} {% trans %}info_providers.bulk_import.parts{% endtrans %}
{{ job.resultCount }} {% trans %}info_providers.bulk_import.results{% endtrans %}
{% trans %}info_providers.bulk_import.created_at{% endtrans %}: {{ job.createdAt|date('Y-m-d H:i') }}
</small>
</div>
<div>
{% if job.isPending %}
<span class="badge bg-warning">{% trans %}info_providers.bulk_import.status.pending{% endtrans %}</span>
{% elseif job.isInProgress %}
<span class="badge bg-info">{% trans %}info_providers.bulk_import.status.in_progress{% endtrans %}</span>
{% elseif job.isCompleted %}
<span class="badge bg-success">{% trans %}info_providers.bulk_import.status.completed{% endtrans %}</span>
{% elseif job.isFailed %}
<span class="badge bg-danger">{% trans %}info_providers.bulk_import.status.failed{% endtrans %}</span>
{% endif %}
</div>
</div>
<!-- Progress Bar -->
<div class="card mb-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0">Progress</h6>
<span data-bulk-import-target="progressText">{{ job.completedPartsCount }} / {{ job.partCount }} completed</span>
</div>
<div class="progress" style="height: 8px;">
<div data-bulk-import-target="progressBar" class="progress-bar" role="progressbar"
style="width: {{ job.progressPercentage }}%"
aria-valuenow="{{ job.progressPercentage }}" aria-valuemin="0" aria-valuemax="100">
</div>
</div>
<div class="d-flex justify-content-between mt-2">
<small class="text-muted">
<span id="completed-count">{{ job.completedPartsCount }}</span> {% trans %}info_providers.bulk_import.completed{% endtrans %}
<span id="skipped-count">{{ job.skippedPartsCount }}</span> {% trans %}info_providers.bulk_import.skipped{% endtrans %}
</small>
<small class="text-muted"><span id="progress-percentage">{{ job.progressPercentage }}%</span></small>
</div>
</div>
</div>
<!-- Tutorial/Instructions -->
<div class="alert alert-info mb-4" role="alert">
<h6 class="alert-heading">
<i class="fas fa-info-circle"></i> {% trans %}info_providers.bulk_import.step2.instructions.title{% endtrans %}
</h6>
<p class="mb-2">{% trans %}info_providers.bulk_import.step2.instructions.description{% endtrans %}</p>
<ul class="mb-0 ps-3">
<li>{% trans %}info_providers.bulk_import.step2.instructions.step1{% endtrans %}</li>
<li>{% trans %}info_providers.bulk_import.step2.instructions.step2{% endtrans %}</li>
<li>{% trans %}info_providers.bulk_import.step2.instructions.step3{% endtrans %}</li>
</ul>
</div>
<!-- Research Controls -->
<div class="card mb-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-1">{% trans %}info_providers.bulk_import.research.title{% endtrans %}</h6>
<small class="text-muted">{% trans %}info_providers.bulk_import.research.description{% endtrans %}</small>
</div>
<div>
<button type="button" class="btn btn-outline-primary btn-sm me-2"
data-action="click->bulk-import#researchAllParts"
id="research-all-btn">
<span class="spinner-border spinner-border-sm me-1" style="display: none;" id="research-all-spinner"></span>
<i class="fas fa-search"></i> {% trans %}info_providers.bulk_import.research.all_pending{% endtrans %}
</button>
</div>
</div>
</div>
</div>
{% for part_result in search_results %}
{# @var part_result \App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultsDTO #}
{% set part = part_result.part %}
{% set isCompleted = job.isPartCompleted(part.id) %}
{% set isSkipped = job.isPartSkipped(part.id) %}
<div class="card mb-3 {% if isCompleted %}border-success{% elseif isSkipped %}border-warning{% endif %}"
data-part-id="{{ part.id }}"
{% if isCompleted %}style="background-color: rgba(25, 135, 84, 0.1);"{% endif %}>
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<h5 class="card-title mb-0">
<a href="{{ path('app_part_show', {'id': part.id}) }}" class="text-decoration-none">
{{ part.name }}
</a>
{% if isCompleted %}
<span class="badge bg-success">
<i class="fas fa-check"></i> {% trans %}info_providers.bulk_import.completed{% endtrans %}
</span>
{% elseif isSkipped %}
<span class="badge bg-warning">
<i class="fas fa-forward"></i> {% trans %}info_providers.bulk_import.skipped{% endtrans %}
</span>
{% endif %}
{% if part_result.errors is not empty %}
<span class="badge bg-danger">{% trans with {'%count%': part_result.errors|length} %}info_providers.bulk_import.errors{% endtrans %}</span>
{% endif %}
<span class="badge bg-info">{% trans with {'%count%': part_result.searchResults|length} %}info_providers.bulk_import.results_found{% endtrans %}</span>
</h5>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-info btn-sm"
data-action="click->bulk-import#researchPart"
data-part-id="{{ part.id }}"
title="{% trans %}info_providers.bulk_import.research.part_tooltip{% endtrans %}">
<span class="spinner-border spinner-border-sm me-1" style="display: none;" data-research-spinner="{{ part.id }}"></span>
<i class="fas fa-search"></i> {% trans %}info_providers.bulk_import.research.part{% endtrans %}
</button>
{% if not isCompleted and not isSkipped %}
<button type="button" class="btn btn-success btn-sm" data-action="click->bulk-import#markCompleted" data-part-id="{{ part.id }}">
<i class="fas fa-check"></i> {% trans %}info_providers.bulk_import.mark_completed{% endtrans %}
</button>
<button type="button" class="btn btn-warning btn-sm" data-action="click->bulk-import#markSkipped" data-part-id="{{ part.id }}">
<i class="fas fa-forward"></i> {% trans %}info_providers.bulk_import.mark_skipped{% endtrans %}
</button>
{% elseif isCompleted %}
<button type="button" class="btn btn-outline-secondary btn-sm" data-action="click->bulk-import#markPending" data-part-id="{{ part.id }}">
<i class="fas fa-undo"></i> {% trans %}info_providers.bulk_import.mark_pending{% endtrans %}
</button>
{% elseif isSkipped %}
<button type="button" class="btn btn-outline-secondary btn-sm" data-action="click->bulk-import#markPending" data-part-id="{{ part.id }}">
<i class="fas fa-undo"></i> {% trans %}info_providers.bulk_import.mark_pending{% endtrans %}
</button>
{% endif %}
</div>
</div>
<div class="card-body">
{% if part_result.errors is not empty %}
{% for error in part_result.errors %}
<div class="alert alert-warning" role="alert">
<i class="fas fa-exclamation-triangle"></i>
{{ error }}
</div>
{% endfor %}
{% endif %}
{% if part_result.searchResults|length > 0 %}
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th></th>
<th>{% trans %}name.label{% endtrans %}</th>
<th>{% trans %}description.label{% endtrans %}</th>
<th>{% trans %}manufacturer.label{% endtrans %}</th>
<th>{% trans %}info_providers.table.provider.label{% endtrans %}</th>
<th>{% trans %}info_providers.bulk_import.source_field{% endtrans %}</th>
<th>{% trans %}info_providers.bulk_import.action.label{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for result in part_result.searchResults %}
{# @var result \App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultDTO #}
{% set dto = result.searchResult %}
{% set localPart = result.localPart %}
<tr>
<td>
<img src="{{ dto.preview_image_url }}" data-thumbnail="{{ dto.preview_image_url }}"
class="hoverpic" style="max-width: 35px;" {{ stimulus_controller('elements/hoverpic') }}>
</td>
<td>
{% if dto.provider_url is not null %}
<a href="{{ dto.provider_url }}" target="_blank" rel="noopener">{{ dto.name }}</a>
{% else %}
{{ dto.name }}
{% endif %}
{% if dto.mpn is not null %}
<br><small class="text-muted">{{ dto.mpn }}</small>
{% endif %}
</td>
<td>{{ dto.description }}</td>
<td>{{ dto.manufacturer ?? '' }}</td>
<td>
{{ info_provider_label(dto.provider_key)|default(dto.provider_key) }}
<br><small class="text-muted">{{ dto.provider_id }}</small>
</td>
<td>
<span class="badge bg-info">{{ result.sourceField ?? 'unknown' }}</span>
{% if result.sourceKeyword %}
<br><small class="text-muted">{{ result.sourceKeyword }}</small>
{% endif %}
</td>
<td>
<div class="btn-group-vertical btn-group-sm" role="group">
{% set updateHref = path('info_providers_update_part',
{'id': part.id, 'providerKey': dto.provider_key, 'providerId': dto.provider_id}) ~ '?jobId=' ~ job.id %}
<a class="btn btn-primary{% if isCompleted %} disabled{% endif %}" href="{% if not isCompleted %}{{ updateHref }}{% else %}#{% endif %}"{% if isCompleted %} aria-disabled="true"{% endif %}>
<i class="fas fa-edit"></i> {% trans %}info_providers.bulk_import.update_part{% endtrans %}
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info" role="alert">
{% trans %}info_providers.search.no_results{% endtrans %}
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% endblock %}

View file

@ -13,7 +13,6 @@
{% else %}
{{ provider.providerInfo.name | trans }}
{% endif %}
</h5>
<div>
{% if provider.providerInfo.description is defined and provider.providerInfo.description is not null %}
@ -23,6 +22,11 @@
</div>
<div class="col-6">
{% if provider.providerInfo.settings_class is defined %}
<a href="{{ path('info_providers_provider_settings', {'provider': provider.providerKey}) }}" class="btn btn-primary btn-sm {% if not is_granted('@config.change_system_settings') %}disabled{% endif %}"
title="{% trans %}info_providers.settings.title{% endtrans %}"
><i class="fa-solid fa-cog"></i></a>
{% endif %}
{% for capability in provider.capabilities %}
{# @var capability \App\Services\InfoProviderSystem\Providers\ProviderCapabilities #}
<span class="badge text-bg-secondary">
@ -52,4 +56,4 @@
{% endfor %}
</tbody>
</table>
{% endmacro %}
{% endmacro %}

View file

@ -0,0 +1,31 @@
{% extends "main_card.html.twig" %}
{% macro genId(widget) %}{{ widget.vars.full_name }}{% endmacro %}
{% form_theme form "form/settings_form.html.twig" %}
{% block title %}{% trans %}info_providers.settings.title{% endtrans %}: {{ info_provider_info.name }}{% endblock %}
{% block card_title %}<i class="fa-solid fa-gear fa-fw"></i> {% trans %}info_providers.settings.title{% endtrans %}: <b>{{ info_provider_info.name }}</b>{% endblock %}
{% block card_content %}
<div class="offset-sm-3">
<h3>
{% if info_provider_info.url %}
<a href="{{ info_provider_info.url }}" class="link-external" target="_blank" rel="nofollow">{{ info_provider_info.name }}</a>
{% else %}
{{ info_provider_info.name }}
{% endif %}
</h3>
{% if info_provider_info.description %}
<p class="text-muted">{{ info_provider_info.description }}</p>
{% endif %}
</div>
{{ form_start(form) }}
<div class="row">
<div class="offset-sm-3 col mb-3 ps-2">
<b>{{ form_help(form) }}</b>
</div>
</div>
{{ form_end(form) }}
{% endblock %}

View file

@ -10,6 +10,9 @@
{% block card_content %}
{{ form_start(form) }}
{# Default submit to use when pressing enter. #}
<input type="submit" name="label_dialog[update]" class="d-none">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" id="common-tab" role="tab" aria-controls="common" aria-selected="true" href="#common"
@ -112,6 +115,10 @@
</div>
{% endif %}
{% if form.update_profile is defined %}
{{ form_row(form.update_profile) }}
{% endif %}
<div class="form-group row">
<div class="offset-sm-3 col-sm-9">
<div class="input-group">
@ -145,4 +152,4 @@
</object>
</div>
{% endif %}
{% endblock %}
{% endblock %}

View file

@ -1,9 +1,11 @@
{% import "vars.macro.twig" as vars %}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{{ meta_title }}</title>
<meta name="author" content="{{ partdb_title }}">
<meta name="author" content="{{ vars.partdb_title() }}">
<meta name="description" content="Label for {{ meta_title }}">
<meta name="keywords" content="Part-DB, Label, Barcode">
<style>
@ -14,7 +16,10 @@
</head>
<body>
{% set start_page %}
<div class="page" style="break-after: always;"><table col="{{ options.xcount }}" row="{{ options.ycount }}"><tr>
{# The page div ensures the page breaks, while the page-inner elements restrict the content to the page size. Sine dompdf 3.1.1 we cannot apply the position: absolute; to the page element directly. #}
<div class="page">
<div class="page-inner">
<table col="{{ options.xcount }}" row="{{ options.ycount }}"><tr>
{% endset %}
{% set end_page %}
@ -60,4 +65,4 @@
{% endfor %}
{{ end_page }}
</body>
</html>
</html>

View file

@ -3,17 +3,31 @@
}
.page {
/** We cannot apply the position: absolute trick here, because then dompdf will not respect the page break anymore **/
page-break-inside: avoid;
page-break-before: avoid;
page-break-after: always;
overflow: hidden;
width: 100%;
height: 100%;
}
.page-inner {
/* Absolute position prevents automatic page breaks */
/*position: absolute;*/
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
page-break-inside: avoid;
page-break-before: avoid;
page-break-after: avoid;
}
/* Last page should not break */

View file

@ -1,3 +1,5 @@
{% import "vars.macro.twig" as vars %}
{% apply inky_to_html|inline_css(source('@css/email/foundation-emails.css'), source('@css/email/email.css')) %}
<container>
@ -5,7 +7,7 @@
<row class="header">
<columns>
<spacer size="16"></spacer>
<h4 class="text-center"><a href="{{ url('homepage') }}">{{ partdb_title }}</a></h4>
<h4 class="text-center"><a href="{{ url('homepage') }}">{{ vars.partdb_title() }}</a></h4>
</columns>
</row>

View file

@ -1,5 +1,16 @@
{{ form_row(form.needsReview) }}
{{ form_row(form.favorite) }}
{{ form_row(form.mass) }}
{{ form_row(form.ipn) }}
{{ form_row(form.partUnit) }}
<div {{ stimulus_controller('elements/ipn_suggestion', {
partId: part.id,
partCategoryId: part.category ? part.category.id : null,
partDescription: part.description,
suggestions: ipnSuggestions,
'commonSectionHeader': 'part.edit.tab.advanced.ipn.commonSectionHeader'|trans,
'partIncrementHeader': 'part.edit.tab.advanced.ipn.partIncrementHeader'|trans,
'suggestUrl': url('ipn_suggestions')
}) }}>
{{ form_row(form.ipn) }}
</div>
{{ form_row(form.partUnit) }}
{{ form_row(form.partCustomState) }}

View file

@ -4,6 +4,32 @@
{% trans with {'%name%': part.name|escape } %}part.edit.title{% endtrans %}
{% endblock %}
{% block before_card %}
{% if bulk_job and jobId %}
<div class="alert alert-info mb-3">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<a href="{{ path('bulk_info_provider_step2', {jobId: bulk_job.id}) }}" class="btn btn-outline-primary btn-sm me-2">
<i class="fas fa-arrow-left fa-fw" aria-hidden="true"></i>
{% trans %}info_providers.bulk_import.back{% endtrans %}
</a>
<form method="post" action="{{ path('part_bulk_import_complete', {id: part.id, jobId: bulk_job.id}) }}" style="display: inline;">
<input type="hidden" name="_token" value="{{ csrf_token('bulk_complete_' ~ part.id) }}">
<button type="submit" class="btn btn-primary btn-sm me-3">
<i class="fas fa-check fa-fw" aria-hidden="true"></i>
{% trans %}info_providers.bulk_import.complete{% endtrans %}
</button>
</form>
<div>
<i class="fas fa-cloud-download fa-fw" aria-hidden="true"></i>
{% trans %}info_providers.bulk_import.editing_part{% endtrans %}
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block card_title %}
<i class="fas fa-edit fa-fw" aria-hidden="true"></i>
{% trans with {'%name%': part.name|escape } %}part.edit.card_title{% endtrans %}

View file

@ -5,6 +5,19 @@
{% block card_border %}border-info{% endblock %}
{% block card_type %}bg-info text-bg-info{% endblock %}
{% block before_card %}
{% if bulk_job and jobId %}
<div class="alert alert-info mb-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<i class="fas fa-cloud-download fa-fw" aria-hidden="true"></i>
{% trans %}info_providers.bulk_import.editing_part{% endtrans %}
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block title %}
{% trans %}info_providers.update_part.title{% endtrans %}: {{ merge_old_name }}
{% endblock %}

View file

@ -13,7 +13,7 @@
<div class="carousel-item {% if loop.first %}active{% endif %}">
<a href="{{ entity_url(pic, 'file_view') }}" data-turbo="false" target="_blank" rel="noopener">
<img class="d-block w-100 img-fluid img-thumbnail bg-light part-info-image" src="{{ entity_url(pic, 'file_view') }}" alt="">
{% if img_overlay %}
{% if settings_instance("part_info").showPartImageOverlay %}
<div class="mask"></div>
<div class="carousel-caption-hover">
<div class="carousel-caption text-white">

View file

@ -36,6 +36,19 @@
</div>
{% endif %}
{% if part.partCustomState is not null %}
<div class="mt-1">
<h6>
<span class="badge bg-primary" title="{% trans %}part_custom_state.caption{% endtrans %}"><i class="fas fa-tools fa-fw"></i> {{ part.partCustomState.name }}</span>
{% if part.partCustomState is not null and part.partCustomState.masterPictureAttachment and attachment_manager.fileExisting(part.partCustomState.masterPictureAttachment) %}
<br/>
<img class="img-fluid img-thumbnail thumbnail-sm" src="{{ attachment_thumbnail(part.partCustomState.masterPictureAttachment, 'thumbnail_md') }}" alt="{% trans %}attachment.preview.alt{% endtrans %}" />
{% endif %}
</h6>
</div>
{% endif %}
{# Favorite Status tag #}
{% if part.favorite %}
<div class="mt-1">
@ -79,4 +92,4 @@
</a>
</h6>
</div>
{% endif %}
{% endif %}

View file

@ -4,6 +4,9 @@
{% for name, parameters in part.groupedParameters %}
{% if name is not empty %}<h5 class="mt-1">{{ name }}</h5>{% endif %}
{{ helper.parameters_table(parameters) }}
{% if not loop.last %}
<hr class="my-0">
{% endif %}
{% endfor %}
{% if description_params is not empty %}
@ -14,4 +17,4 @@
{% if comment_params is not empty %}
<h5 class="mt-1">{% trans %}parameters.auto_extracted_from_comment{% endtrans %}</h5>
{{ helper.parameters_table(comment_params) }}
{% endif %}
{% endif %}

View file

@ -31,6 +31,11 @@
<button class="nav-link" id="filter-projects-tab" data-bs-toggle="tab" data-bs-target="#filter-projects"><i class="fas fa-archive fa-fw"></i> {% trans %}project.labelp{% endtrans %}</button>
</li>
{% endif %}
{% if filterForm.inBulkImportJob is defined %}
<li class="nav-item" role="presentation">
<button class="nav-link" id="filter-bulk-import-tab" data-bs-toggle="tab" data-bs-target="#filter-bulk-import"><i class="fas fa-download fa-fw"></i> {% trans %}part.edit.tab.bulk_import{% endtrans %}</button>
</li>
{% endif %}
</ul>
{{ form_start(filterForm, {"attr": {"data-controller": "helpers--form-cleanup", "data-action": "helpers--form-cleanup#submit"}}) }}
@ -56,6 +61,7 @@
{{ form_row(filterForm.favorite) }}
{{ form_row(filterForm.needsReview) }}
{{ form_row(filterForm.measurementUnit) }}
{{ form_row(filterForm.partCustomState) }}
{{ form_row(filterForm.mass) }}
{{ form_row(filterForm.dbId) }}
{{ form_row(filterForm.ipn) }}
@ -126,6 +132,13 @@
{{ form_row(filterForm.bomComment) }}
</div>
{% endif %}
{% if filterForm.inBulkImportJob is defined %}
<div class="tab-pane pt-3" id="filter-bulk-import" role="tabpanel" aria-labelledby="filter-bulk-import-tab" tabindex="0">
{{ form_row(filterForm.inBulkImportJob) }}
{{ form_row(filterForm.bulkImportJobStatus) }}
{{ form_row(filterForm.bulkImportPartStatus) }}
</div>
{% endif %}
</div>

View file

@ -0,0 +1,186 @@
{# BOM Validation Results Component #}
{#
Usage:
{% include 'projects/_bom_validation_results.html.twig' with {
validation_result: validation_result,
show_summary: true,
show_details: true
} %}
#}
{% if validation_result is defined and validation_result is not empty %}
{% set stats = validation_result %}
{# Validation Summary #}
{% if show_summary is defined and show_summary %}
<div class="row mb-3">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fa-solid fa-chart-bar fa-fw"></i>
{% trans %}project.bom_import.validation.summary{% endtrans %}
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<div class="text-center">
<div class="h3 text-primary">{{ stats.total_entries }}</div>
<small class="text-muted">{% trans %}project.bom_import.validation.total_entries{% endtrans %}</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<div class="h3 text-success">{{ stats.valid_entries }}</div>
<small class="text-muted">{% trans %}project.bom_import.validation.valid_entries{% endtrans %}</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<div class="h3 text-warning">{{ stats.invalid_entries }}</div>
<small class="text-muted">{% trans %}project.bom_import.validation.invalid_entries{% endtrans %}</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<div class="h3 text-info">
{% if stats.total_entries > 0 %}
{{ ((stats.valid_entries / stats.total_entries) * 100) | round(1) }}%
{% else %}
0%
{% endif %}
</div>
<small class="text-muted">{% trans %}project.bom_import.validation.success_rate{% endtrans %}</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
{# Validation Messages #}
{% if validation_result.errors is defined and validation_result.errors is not empty %}
<div class="alert alert-danger">
<h4><i class="fa-solid fa-exclamation-triangle fa-fw"></i> {% trans %}project.bom_import.validation.errors.title{% endtrans %}</h4>
<p class="mb-2">{% trans %}project.bom_import.validation.errors.description{% endtrans %}</p>
<ul class="mb-0">
{% for error in validation_result.errors %}
<li>{{ error|raw }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if validation_result.warnings is defined and validation_result.warnings is not empty %}
<div class="alert alert-warning">
<h4><i class="fa-solid fa-exclamation-circle fa-fw"></i> {% trans %}project.bom_import.validation.warnings.title{% endtrans %}</h4>
<p class="mb-2">{% trans %}project.bom_import.validation.warnings.description{% endtrans %}</p>
<ul class="mb-0">
{% for warning in validation_result.warnings %}
<li>{{ warning|raw }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if validation_result.info is defined and validation_result.info is not empty %}
<div class="alert alert-info">
<h4><i class="fa-solid fa-info-circle fa-fw"></i> {% trans %}project.bom_import.validation.info.title{% endtrans %}</h4>
<ul class="mb-0">
{% for info in validation_result.info %}
<li>{{ info|raw }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{# Detailed Line-by-Line Results #}
{% if show_details is defined and show_details and validation_result.line_results is defined %}
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fa-solid fa-list fa-fw"></i>
{% trans %}project.bom_import.validation.details.title{% endtrans %}
</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>{% trans %}project.bom_import.validation.details.line{% endtrans %}</th>
<th>{% trans %}project.bom_import.validation.details.status{% endtrans %}</th>
<th>{% trans %}project.bom_import.validation.details.messages{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for line_result in validation_result.line_results %}
<tr class="{% if line_result.is_valid %}table-success{% else %}table-danger{% endif %}">
<td>
<strong>{{ line_result.line_number }}</strong>
</td>
<td>
{% if line_result.is_valid %}
<span class="badge bg-success">
<i class="fa-solid fa-check fa-fw"></i>
{% trans %}project.bom_import.validation.details.valid{% endtrans %}
</span>
{% else %}
<span class="badge bg-danger">
<i class="fa-solid fa-times fa-fw"></i>
{% trans %}project.bom_import.validation.details.invalid{% endtrans %}
</span>
{% endif %}
</td>
<td>
{% if line_result.errors is not empty %}
<div class="text-danger">
{% for error in line_result.errors %}
<div><i class="fa-solid fa-exclamation-triangle fa-fw"></i> {{ error|raw }}</div>
{% endfor %}
</div>
{% endif %}
{% if line_result.warnings is not empty %}
<div class="text-warning">
{% for warning in line_result.warnings %}
<div><i class="fa-solid fa-exclamation-circle fa-fw"></i> {{ warning|raw }}</div>
{% endfor %}
</div>
{% endif %}
{% if line_result.info is not empty %}
<div class="text-info">
{% for info in line_result.info %}
<div><i class="fa-solid fa-info-circle fa-fw"></i> {{ info|raw }}</div>
{% endfor %}
</div>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
{# Action Buttons #}
{% if validation_result.is_valid is defined %}
<div class="mt-3">
{% if validation_result.is_valid %}
<div class="alert alert-success">
<i class="fa-solid fa-check-circle fa-fw"></i>
{% trans %}project.bom_import.validation.all_valid{% endtrans %}
</div>
{% else %}
<div class="alert alert-danger">
<i class="fa-solid fa-exclamation-triangle fa-fw"></i>
{% trans %}project.bom_import.validation.fix_errors{% endtrans %}
</div>
{% endif %}
</div>
{% endif %}
{% endif %}

View file

@ -8,7 +8,8 @@
{% endblock %}
{% block card_content %}
{% set can_build = buildHelper.projectBuildable(project, number_of_builds) %}
{% set bom_empty = project.bomEntries | length == 0 %}
{% set can_build = not bom_empty and buildHelper.projectBuildable(project, number_of_builds) %}
{% import "components/projects.macro.html.twig" as project_macros %}
{% if project.status is not empty and project.status != "in_production" %}
@ -17,8 +18,10 @@
</div>
{% endif %}
<div class="alert {% if can_build %}alert-success{% else %}alert-danger{% endif %}" role="alert">
{% if not can_build %}
<div class="alert {% if can_build %}alert-success{% elseif bom_empty%}alert-warning{% else %}alert-danger{% endif %}" role="alert">
{% if bom_empty %}
<h5><i class="fa-solid fa-circle-exclamation fa-fw"></i> {% trans %}project.builds.no_bom_entries{% endtrans %}</h5>
{% elseif not can_build %}
<h5><i class="fa-solid fa-circle-exclamation fa-fw"></i> {% trans %}project.builds.build_not_possible{% endtrans %}</h5>
<b>{% trans with {"%number_of_builds%": number_of_builds} %}project.builds.following_bom_entries_miss_instock_n{% endtrans %}</b>
<ul>
@ -37,4 +40,4 @@
{% include 'projects/build/_form.html.twig' %}
{% endblock %}
{% endblock %}

View file

@ -0,0 +1,204 @@
{% extends "main_card.html.twig" %}
{% block title %}{% trans %}project.bom_import.map_fields{% endtrans %}{% endblock %}
{% block card_title %}
<i class="fa-solid fa-arrows-left-right fa-fw"></i>
{% trans %}project.bom_import.map_fields{% endtrans %}{% if project %}: <i>{{ project.name }}</i>{% endif %}
{% endblock %}
{% block card_content %}
{% if validation_result is defined %}
{% include 'projects/_bom_validation_results.html.twig' with {
validation_result: validation_result,
show_summary: true,
show_details: false
} %}
{% endif %}
<div class="row mb-3">
<div class="col-12">
<div class="alert alert-info">
<i class="fa-solid fa-info-circle fa-fw"></i>
{% trans %}project.bom_import.map_fields.help{% endtrans %}
</div>
<div class="alert alert-warning">
<i class="fa-solid fa-lightbulb fa-fw"></i>
{% trans %}project.bom_import.field_mapping.priority_note{% endtrans %}
</div>
</div>
</div>
{{ form_start(form) }}
<div class="row mb-3">
<div class="col-md-6">
{{ form_row(form.delimiter) }}
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fa-solid fa-table-columns fa-fw"></i>
{% trans %}project.bom_import.field_mapping.title{% endtrans %}
</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>{% trans %}project.bom_import.field_mapping.csv_field{% endtrans %}</th>
<th>{% trans %}project.bom_import.field_mapping.maps_to{% endtrans %}</th>
<th>{% trans %}project.bom_import.field_mapping.suggestion{% endtrans %}</th>
<th>{% trans %}project.bom_import.field_mapping.priority{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for field in detected_fields %}
<tr>
<td>
<code>{{ field }}</code>
</td>
<td>
{{ form_widget(form['mapping_' ~ field_name_mapping[field]], {
'attr': {
'class': 'form-select field-mapping-select',
'data-field': field
}
}) }}
</td>
<td>
{% if suggested_mapping[field] is defined %}
<span class="badge bg-success">
<i class="fa-solid fa-magic fa-fw"></i>
{{ suggested_mapping[field] }}
</span>
{% else %}
<span class="text-muted">
<i class="fa-solid fa-question fa-fw"></i>
{% trans %}project.bom_import.field_mapping.no_suggestion{% endtrans %}
</span>
{% endif %}
</td>
<td>
<input type="number"
class="form-control form-control-sm priority-input"
min="1"
value="10"
style="width: 80px;"
data-field="{{ field }}"
title="{% trans %}project.bom_import.field_mapping.priority_help{% endtrans %}">
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="mt-3">
<h6>{% trans %}project.bom_import.field_mapping.summary{% endtrans %}:</h6>
<div id="mapping-summary" class="alert alert-info">
<i class="fa-solid fa-info-circle fa-fw"></i>
{% trans %}project.bom_import.field_mapping.select_to_see_summary{% endtrans %}
</div>
</div>
</div>
</div>
<div class="mt-3">
{{ form_widget(form.submit, {
'attr': {
'class': 'btn btn-primary'
}
}) }}
<a href="{{ path('project_import_bom', {'id': project.id}) }}" class="btn btn-secondary">
<i class="fa-solid fa-arrow-left fa-fw"></i>
{% trans %}common.back{% endtrans %}
</a>
</div>
{{ form_end(form) }}
<script nonce="{{ csp_nonce('script') }}">
// Function to initialize the field mapping page
function initializeFieldMapping() {
const suggestions = {{ suggested_mapping|json_encode|raw }};
const fieldNameMapping = {{ field_name_mapping|json_encode|raw }};
Object.keys(suggestions).forEach(function(field) {
// Use the sanitized field name from the server-side mapping
const sanitizedField = fieldNameMapping[field];
const select = document.querySelector('[name="form[mapping_' + sanitizedField + ']"]');
if (select && !select.value) {
select.value = suggestions[field];
}
});
// Update mapping summary
updateMappingSummary();
// Add event listeners for dynamic updates
document.querySelectorAll('.field-mapping-select').forEach(function(select) {
select.addEventListener('change', updateMappingSummary);
});
document.querySelectorAll('.priority-input').forEach(function(input) {
input.addEventListener('change', updateMappingSummary);
});
}
// Initialize on both DOMContentLoaded and Turbo events
document.addEventListener('DOMContentLoaded', initializeFieldMapping);
document.addEventListener('turbo:load', initializeFieldMapping);
document.addEventListener('turbo:frame-load', function(event) {
// Only initialize if this frame contains our field mapping content
if (event.target.id === 'content' || event.target.closest('#content')) {
initializeFieldMapping();
}
});
function updateMappingSummary() {
const summary = document.getElementById('mapping-summary');
const mappings = {};
const priorities = {};
// Collect all mappings and priorities
document.querySelectorAll('.field-mapping-select').forEach(function(select) {
const field = select.getAttribute('data-field');
const target = select.value;
const priorityInput = document.querySelector('.priority-input[data-field="' + field + '"]');
const priority = priorityInput ? parseInt(priorityInput.value) || 10 : 10;
if (target && target !== '') {
if (!mappings[target]) {
mappings[target] = [];
}
mappings[target].push({
field: field,
priority: priority
});
}
});
// Sort by priority and build summary
let summaryHtml = '<div class="row">';
Object.keys(mappings).forEach(function(target) {
const fieldMappings = mappings[target].sort((a, b) => a.priority - b.priority);
const fieldList = fieldMappings.map(m => m.field + ' (' + '{{ "project.bom_import.field_mapping.priority_short"|trans }}' + m.priority + ')').join(', ');
summaryHtml += '<div class="col-md-6 mb-2">';
summaryHtml += '<strong>' + target + ':</strong> ' + fieldList;
summaryHtml += '</div>';
});
summaryHtml += '</div>';
if (Object.keys(mappings).length === 0) {
summary.innerHTML = '<i class="fa-solid fa-info-circle fa-fw"></i> {{ "project.bom_import.field_mapping.select_to_see_summary"|trans }}';
} else {
summary.innerHTML = summaryHtml;
}
}
</script>
{% endblock %}

View file

@ -1,4 +1,5 @@
{% set can_build = buildHelper.projectBuildable(project) %}
{% set bom_empty = project.bomEntries | length == 0 %}
{% set can_build = not bom_empty and buildHelper.projectBuildable(project) %}
{% import "components/projects.macro.html.twig" as project_macros %}
@ -8,8 +9,10 @@
</div>
{% endif %}
<div class="alert mt-2 {% if can_build %}alert-success{% else %}alert-danger{% endif %}" role="alert">
{% if not can_build %}
<div class="alert mt-2 {% if can_build %}alert-success{% elseif bom_empty%}alert-warning{% else %}alert-danger{% endif %}" role="alert">
{% if bom_empty %}
<h5><i class="fa-solid fa-circle-exclamation fa-fw"></i> {% trans %}project.builds.no_bom_entries{% endtrans %}</h5>
{% elseif not can_build %}
<h5><i class="fa-solid fa-circle-exclamation fa-fw"></i> {% trans %}project.builds.build_not_possible{% endtrans %}</h5>
<b>{% trans %}project.builds.following_bom_entries_miss_instock{% endtrans %}</b>
<ul>
@ -19,7 +22,7 @@
</ul>
{% else %}
<h5><i class="fa-solid fa-circle-check fa-fw"></i> {% trans %}project.builds.build_possible{% endtrans %}</h5>
<span>{% trans with {"%max_builds%": buildHelper.maximumBuildableCount(project)} %}project.builds.number_of_builds_possible{% endtrans %}</span>
<span>{% trans with {"%max_builds%": buildHelper.maximumBuildableCountAsString(project)} %}project.builds.number_of_builds_possible{% endtrans %}</span>
{% endif %}
</div>
@ -27,7 +30,7 @@
<div class="row mt-2">
<div class="col-4">
<div class="input-group mb-3">
<input type="number" min="1" class="form-control" placeholder="{% trans %}project.builds.number_of_builds{% endtrans %}" name="n" required>
<input type="number" min="1" class="form-control" placeholder="{% trans %}project.builds.number_of_builds{% endtrans %}" name="n" required value="1">
<input type="hidden" name="_redirect" value="{{ uri_without_host(app.request) }}">
<button class="btn btn-outline-secondary" type="submit" id="button-addon2">{% trans %}project.build.btn_build{% endtrans %}</button>
</div>
@ -37,4 +40,4 @@
{% if project.buildPart %}
<p><b>{% trans %}project.builds.no_stocked_builds{% endtrans %}:</b> <a href="{{ entity_url(project.buildPart) }}">{{ project.buildPart.amountSum }}</a></p>
{% endif %}
{% endif %}

View file

@ -22,8 +22,7 @@
{% block card_content %}
<form action="{{ path('login') }}" method="post" data-turbo="false" class="form-horizontal">
<input type="hidden" name="_csrf_token"
value="{{ csrf_token('authenticate') }}">
<input type="hidden" name="_csrf_token" data-controller="csrf-protection" value="{{ csrf_token('authenticate') }}">
<input type="hidden" name="_target_path" value="{{ app.request.query.get('_target_path') }}" />
@ -72,4 +71,4 @@
{% if allow_email_pw_reset %}
<a class="offset-sm-2" href="{{ path('pw_reset_request') }}">{% trans %}pw_reset.password_forget{% endtrans %}</a>
{% endif %}
{% endblock %}
{% endblock %}

View file

@ -0,0 +1,66 @@
{% extends "main_card.html.twig" %}
{% macro genId(widget) %}{{ widget.vars.full_name }}{% endmacro %}
{% form_theme form "form/settings_form.html.twig" %}
{% block title %}{% trans %}settings.title{% endtrans %}{% endblock %}
{% block card_title %}<i class="fa-solid fa-gears fa-fw"></i> {% trans %}settings.title{% endtrans %}{% endblock %}
{% block card_content %}
{{ form_start(form) }}
{# Tabs #}
<ul class="nav nav-tabs">
{% for tab_widget in form %}
{# Create a tab for each compound form #}
{% if tab_widget.vars.compound ?? false %}
<li class="nav-item">
<button type="button" class="nav-link {% if loop.first %}active{% endif %}" aria-current="page" data-bs-toggle="tab"
id="settings-{{ _self.genId(tab_widget) }}-tab" data-bs-target="#settings-{{ _self.genId(tab_widget) }}-pane"
>{{ (tab_widget.vars.label ?? tab_widget.vars.name|humanize)|trans }}</button>
</li>
{% endif %}
{% endfor %}
</ul>
{# Panes #}
<div class="tab-content">
{% for tab_widget in form %}
{# Create a tab for each compound form #}
{% if tab_widget.vars.compound ?? false %}
<div class="tab-pane fade pt-2 {% if loop.first %}show active{% endif %}" id="settings-{{ _self.genId(tab_widget) }}-pane">
{{ form_help(tab_widget) }}
{{ form_errors(tab_widget) }}
{% for section_widget in tab_widget %}
{% set settings_object = section_widget.vars.value %}
{% if section_widget.vars.compound ?? false %}
<fieldset>
<legend class="offset-3">
<i class="fa-solid {{ settings_icon(settings_object)|default('fa-sliders') }} fa-fw"></i>
{{ (section_widget.vars.label ?? section_widget.vars.name|humanize)|trans }}
</legend>
<div class="row">
<div class="offset-sm-3 col mb-3 ps-2">
<b>{{ form_help(section_widget) }}</b>
{{ form_errors(section_widget) }}
</div>
</div>
{{ form_widget(section_widget) }}
</fieldset>
{% if not loop.last %}
<hr class="mx-0 mb-2 mt-2">
{% endif %}
{% else %} {# If not a compound render as normal row #}
{{ form_row(section_widget) }}
{% endif %}
{% endfor %}
</div>
{% endif %}
{% endfor %}
</div>
{{ form_end(form) }}
{% endblock %}

View file

@ -1,4 +1,5 @@
{% import "helper.twig" as helper %}
{% import "vars.macro.twig" as vars %}
<table class="table table-sm table-striped table-hover table-bordered">
<tbody>
@ -15,7 +16,7 @@
</tr>
<tr>
<td>Part-DB Instance name</td>
<td>{{ partdb_title }}</td>
<td>{{ vars.partdb_title() }}</td>
</tr>
<tr>
<td>Default locale</td>

View file

@ -1,12 +1,13 @@
{% extends "base.html.twig" %}
{% import "vars.macro.twig" as vars %}
{% block title %}{{ partdb_title }} {% trans %}tfa_backup.codes.title{% endtrans %}{% endblock %}
{% block title %}{{ vars.partdb_title() }} {% trans %}tfa_backup.codes.title{% endtrans %}{% endblock %}
{% block body %}
<div class="container">
<div class="card">
<div class="card-header">
{{ partdb_title }} {% trans %}tfa_backup.codes.title{% endtrans %}
{{ vars.partdb_title() }} {% trans %}tfa_backup.codes.title{% endtrans %}
</div>
<div class="card-body">
<h5 class="card-title">{% trans %}tfa_backup.codes.explanation{% endtrans %}</h5>

View file

@ -0,0 +1,3 @@
{% macro partdb_title() %}{{ settings_instance("customization").instanceName }}{% endmacro %}
{% macro base_currency() %}{{ settings_instance("localization").baseCurrency }}{% endmacro %}