Skip to content

Commit 2e29297

Browse files
author
Marcel Diegelmann
committed
Cleanup-Logik für Baugruppen und BOM-Einträge im Statistik-Bereich überarbeiten bzw. erweitern
1 parent eccfe15 commit 2e29297

35 files changed

+871
-240
lines changed

assets/controllers/elements/part_search_controller.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ export default class extends Controller {
227227
},
228228
templates: {
229229
header({ html }) {
230-
return html`<span class="aa-SourceHeaderTitle">${trans(STATISTICS_ASSEMBLIES)}</span>
230+
return html`<span class="aa-SourceHeaderTitle">${trans("assembly.labelp")}</span>
231231
<div class="aa-SourceHeaderLine" />`;
232232
},
233233
item({ item, components, html }) {
@@ -273,7 +273,7 @@ export default class extends Controller {
273273
},
274274
templates: {
275275
header({ html }) {
276-
return html`<span class="aa-SourceHeaderTitle">${trans(STATISTICS_PROJECTS)}</span>
276+
return html`<span class="aa-SourceHeaderTitle">${trans("project.labelp")}</span>
277277
<div class="aa-SourceHeaderLine" />`;
278278
},
279279
item({ item, components, html }) {

assets/controllers/pages/statistics_assembly_controller.js

Lines changed: 135 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,154 @@ import { Controller } from '@hotwired/stimulus';
22

33
export default class extends Controller {
44
static values = {
5-
url: String,
6-
confirmMsg: String,
7-
successMsg: String,
8-
errorMsg: String
5+
cleanupBomUrl: String,
6+
cleanupPreviewUrl: String
97
}
108

11-
static targets = ["count"]
9+
static targets = ["bomCount", "previewCount", "bomButton", "previewButton"]
1210

1311
async cleanup(event) {
14-
event.preventDefault();
12+
if (event) {
13+
event.preventDefault();
14+
event.stopImmediatePropagation();
15+
}
16+
17+
const button = event ? event.currentTarget : null;
18+
if (button) button.disabled = true;
19+
20+
try {
21+
const data = await this.fetchWithErrorHandling(this.cleanupBomUrlValue, { method: 'POST' });
1522

16-
if (!confirm(this.confirmMsgValue)) {
17-
return;
23+
if (data.success) {
24+
this.showSuccessMessage(data.message);
25+
if (this.hasBomCountTarget) {
26+
this.bomCountTarget.textContent = data.new_count;
27+
}
28+
if (data.new_count === 0 && this.hasBomButtonTarget) {
29+
this.bomButtonTarget.remove();
30+
}
31+
} else {
32+
this.showErrorMessage(data.message || 'BOM cleanup failed');
33+
}
34+
} catch (error) {
35+
this.showErrorMessage(error.message || 'An unexpected error occurred during BOM cleanup');
36+
} finally {
37+
if (button) button.disabled = false;
1838
}
39+
}
40+
41+
async cleanupPreview(event) {
42+
if (event) {
43+
event.preventDefault();
44+
event.stopImmediatePropagation();
45+
}
46+
47+
const button = event ? event.currentTarget : null;
48+
if (button) button.disabled = true;
1949

2050
try {
21-
const response = await fetch(this.urlValue, {
22-
method: 'POST',
23-
headers: {
24-
'X-Requested-With': 'XMLHttpRequest'
51+
const data = await this.fetchWithErrorHandling(this.cleanupPreviewUrlValue, { method: 'POST' });
52+
53+
if (data.success) {
54+
this.showSuccessMessage(data.message);
55+
if (this.hasPreviewCountTarget) {
56+
this.previewCountTarget.textContent = data.new_count;
2557
}
26-
});
27-
28-
if (response.ok) {
29-
const data = await response.json();
30-
alert(this.successMsgValue.replace('%count%', data.count));
31-
// Update the count displayed in the UI
32-
if (this.hasCountTarget) {
33-
this.countTarget.innerText = '0';
58+
if (data.new_count === 0 && this.hasPreviewButtonTarget) {
59+
this.previewButtonTarget.remove();
3460
}
35-
// Reload page to reflect changes if needed, or just let the user see 0
36-
window.location.reload();
3761
} else {
38-
alert(this.errorMsgValue);
62+
this.showErrorMessage(data.message || 'Preview cleanup failed');
3963
}
4064
} catch (error) {
41-
console.error('Cleanup failed:', error);
42-
alert(this.errorMsgValue);
65+
this.showErrorMessage(error.message || 'An unexpected error occurred during Preview cleanup');
66+
} finally {
67+
if (button) button.disabled = false;
68+
}
69+
}
70+
71+
getHeaders() {
72+
return {
73+
'X-Requested-With': 'XMLHttpRequest',
74+
'Accept': 'application/json',
4375
}
4476
}
77+
78+
async fetchWithErrorHandling(url, options = {}, timeout = 30000) {
79+
const controller = new AbortController()
80+
const timeoutId = setTimeout(() => controller.abort(), timeout)
81+
82+
try {
83+
const response = await fetch(url, {
84+
...options,
85+
headers: { ...this.getHeaders(), ...options.headers },
86+
signal: controller.signal
87+
})
88+
89+
clearTimeout(timeoutId)
90+
91+
if (!response.ok) {
92+
const errorText = await response.text()
93+
let errorMessage = `Server error (${response.status})`;
94+
try {
95+
const errorJson = JSON.parse(errorText);
96+
if (errorJson && errorJson.message) {
97+
errorMessage = errorJson.message;
98+
}
99+
} catch (e) {
100+
// Not a JSON response, use status text
101+
errorMessage = `${errorMessage}: ${errorText}`;
102+
}
103+
throw new Error(errorMessage)
104+
}
105+
106+
return await response.json()
107+
} catch (error) {
108+
clearTimeout(timeoutId)
109+
110+
if (error.name === 'AbortError') {
111+
throw new Error('Request timed out. Please try again.')
112+
} else if (error.message.includes('Failed to fetch')) {
113+
throw new Error('Network error. Please check your connection and try again.')
114+
} else {
115+
throw error
116+
}
117+
}
118+
}
119+
120+
showSuccessMessage(message) {
121+
this.showToast('success', message)
122+
}
123+
124+
showErrorMessage(message) {
125+
this.showToast('error', message)
126+
}
127+
128+
showToast(type, message) {
129+
// Create a simple alert that doesn't disrupt layout
130+
const alertId = 'alert-' + Date.now();
131+
const iconClass = type === 'success' ? 'fa-check-circle' : 'fa-exclamation-triangle';
132+
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
133+
134+
const alertHTML = `
135+
<div class="alert ${alertClass} alert-dismissible fade show position-fixed"
136+
style="top: 20px; right: 20px; z-index: 9999; max-width: 400px;"
137+
id="${alertId}" role="alert">
138+
<i class="fas ${iconClass} me-2"></i>
139+
${message}
140+
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
141+
</div>
142+
`;
143+
144+
// Add alert to body
145+
document.body.insertAdjacentHTML('beforeend', alertHTML);
146+
147+
// Auto-remove after 5 seconds if not closed manually
148+
setTimeout(() => {
149+
const elementToRemove = document.getElementById(alertId);
150+
if (elementToRemove) {
151+
elementToRemove.remove();
152+
}
153+
}, 5000);
154+
}
45155
}

src/Controller/StatisticsController.php

Lines changed: 84 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,13 @@
4343

4444
use App\Services\Tools\StatisticsHelper;
4545
use App\Entity\AssemblySystem\AssemblyBOMEntry;
46+
use App\Entity\AssemblySystem\Assembly;
4647
use Doctrine\ORM\EntityManagerInterface;
4748
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
4849
use Symfony\Component\HttpFoundation\JsonResponse;
4950
use Symfony\Component\HttpFoundation\Response;
5051
use Symfony\Component\Routing\Attribute\Route;
52+
use Symfony\Contracts\Translation\TranslatorInterface;
5153

5254
class StatisticsController extends AbstractController
5355
{
@@ -62,31 +64,95 @@ public function showStatistics(StatisticsHelper $helper): Response
6264
}
6365

6466
#[Route(path: '/statistics/cleanup-assembly-bom-entries', name: 'statistics_cleanup_assembly_bom_entries', methods: ['POST'])]
65-
public function cleanupAssemblyBOMEntries(EntityManagerInterface $em): JsonResponse
66-
{
67+
public function cleanupAssemblyBOMEntries(
68+
EntityManagerInterface $em,
69+
StatisticsHelper $helper,
70+
TranslatorInterface $translator
71+
): JsonResponse {
6772
$this->denyAccessUnlessGranted('@tools.statistics');
6873

69-
$qb = $em->createQueryBuilder();
70-
$qb->select('be', 'IDENTITY(be.part) AS part_id')
71-
->from(AssemblyBOMEntry::class, 'be')
72-
->leftJoin('be.part', 'p')
73-
->where('be.part IS NOT NULL')
74-
->andWhere('p.id IS NULL');
74+
try {
75+
// We fetch the IDs of the entries that have a non-existent part.
76+
// We use a raw SQL approach or a more robust DQL to avoid proxy initialization issues.
77+
$qb = $em->createQueryBuilder();
78+
$qb->select('be.id', 'IDENTITY(be.part) AS part_id')
79+
->from(AssemblyBOMEntry::class, 'be')
80+
->leftJoin('be.part', 'p')
81+
->where('be.part IS NOT NULL')
82+
->andWhere('p.id IS NULL');
83+
84+
$results = $qb->getQuery()->getResult();
85+
$count = count($results);
86+
87+
foreach ($results as $result) {
88+
$entryId = $result['id'];
89+
$partId = $result['part_id'] ?? 'unknown';
7590

76-
$results = $qb->getQuery()->getResult();
77-
$count = count($results);
91+
$entry = $em->find(AssemblyBOMEntry::class, $entryId);
92+
if ($entry instanceof AssemblyBOMEntry) {
93+
$entry->setPart(null);
94+
$entry->setName(sprintf('part-id=%s not found', $partId));
95+
}
96+
}
7897

79-
foreach ($results as $result) {
80-
/** @var AssemblyBOMEntry $entry */
81-
$entry = $result[0];
82-
$part_id = $result['part_id'] ?? 'unknown';
98+
$em->flush();
8399

84-
$entry->setPart(null);
85-
$entry->setName(sprintf('part-id=%s not found', $part_id));
100+
return new JsonResponse([
101+
'success' => true,
102+
'count' => $count,
103+
'message' => $translator->trans('statistics.cleanup_assembly_bom_entries.success', [
104+
'%count%' => $count,
105+
]),
106+
'new_count' => $helper->getInvalidPartBOMEntriesCount(),
107+
]);
108+
} catch (\Exception $e) {
109+
return new JsonResponse([
110+
'success' => false,
111+
'message' => $translator->trans('statistics.cleanup_assembly_bom_entries.error') . ' ' . $e->getMessage(),
112+
], Response::HTTP_INTERNAL_SERVER_ERROR);
86113
}
114+
}
115+
116+
#[Route(path: '/statistics/cleanup-assembly-preview-attachments', name: 'statistics_cleanup_assembly_preview_attachments', methods: ['POST'])]
117+
public function cleanupAssemblyPreviewAttachments(
118+
EntityManagerInterface $em,
119+
StatisticsHelper $helper,
120+
TranslatorInterface $translator
121+
): JsonResponse {
122+
$this->denyAccessUnlessGranted('@tools.statistics');
123+
124+
try {
125+
$qb = $em->createQueryBuilder();
126+
$qb->select('a')
127+
->from(Assembly::class, 'a')
128+
->leftJoin('a.master_picture_attachment', 'm')
129+
->where('a.master_picture_attachment IS NOT NULL')
130+
->andWhere('m.id IS NULL');
131+
132+
$assemblies = $qb->getQuery()->getResult();
133+
$count = count($assemblies);
87134

88-
$em->flush();
135+
foreach ($assemblies as $assembly) {
136+
if ($assembly instanceof Assembly) {
137+
$assembly->setMasterPictureAttachment(null);
138+
}
139+
}
89140

90-
return new JsonResponse(['success' => true, 'count' => $count]);
141+
$em->flush();
142+
143+
return new JsonResponse([
144+
'success' => true,
145+
'count' => $count,
146+
'message' => $translator->trans('statistics.cleanup_assembly_preview_attachments.success', [
147+
'%count%' => $count,
148+
]),
149+
'new_count' => $helper->getInvalidAssemblyPreviewAttachmentsCount(),
150+
]);
151+
} catch (\Exception $e) {
152+
return new JsonResponse([
153+
'success' => false,
154+
'message' => $translator->trans('statistics.cleanup_assembly_preview_attachments.error') . ' ' . $e->getMessage(),
155+
], Response::HTTP_INTERNAL_SERVER_ERROR);
156+
}
91157
}
92158
}

src/Entity/Attachments/AssemblyAttachment.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
* @extends Attachment<Assembly>
3434
*/
3535
#[UniqueEntity(['name', 'attachment_type', 'element'])]
36-
#[UniqueEntity(['name', 'attachment_type', 'element'])]
3736
#[ORM\Entity]
3837
class AssemblyAttachment extends Attachment
3938
{

src/Services/Tools/StatisticsHelper.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,4 +190,19 @@ public function getInvalidPartBOMEntriesCount(): int
190190

191191
return (int) $qb->getQuery()->getSingleScalarResult();
192192
}
193+
194+
/**
195+
* Returns the number of assemblies that have a master_picture_attachment that does not exist anymore.
196+
*/
197+
public function getInvalidAssemblyPreviewAttachmentsCount(): int
198+
{
199+
$qb = $this->em->createQueryBuilder();
200+
$qb->select('COUNT(a.id)')
201+
->from(Assembly::class, 'a')
202+
->leftJoin('a.master_picture_attachment', 'at')
203+
->where('a.master_picture_attachment IS NOT NULL')
204+
->andWhere('at.id IS NULL');
205+
206+
return (int) $qb->getQuery()->getSingleScalarResult();
207+
}
193208
}

templates/tools/statistics/statistics.html.twig

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,8 @@
6464

6565
<div class="tab-pane fade" id="assemblies" role="tabpanel" aria-labelledby="assemblies-tab"
6666
{{ stimulus_controller('pages/statistics_assembly', {
67-
url: path('statistics_cleanup_assembly_bom_entries'),
68-
confirmMsg: 'statistics.cleanup_assembly_bom_entries.confirm'|trans,
69-
successMsg: 'statistics.cleanup_assembly_bom_entries.success'|trans,
70-
errorMsg: 'statistics.cleanup_assembly_bom_entries.error'|trans
67+
cleanupBomUrl: path('statistics_cleanup_assembly_bom_entries'),
68+
cleanupPreviewUrl: path('statistics_cleanup_assembly_preview_attachments')
7169
}) }}
7270
>
7371
<table class="table table-striped table-hover">
@@ -85,14 +83,25 @@
8583
<tr>
8684
<td>{% trans %}statistics.invalid_part_bom_entries_count{% endtrans %}</td>
8785
<td>
88-
<span {{ stimulus_target('pages/statistics_assembly', 'count') }}>{{ helper.invalidPartBOMEntriesCount }}</span>
86+
<span {{ stimulus_target('pages/statistics_assembly', 'bomCount') }}>{{ helper.invalidPartBOMEntriesCount }}</span>
8987
{% if helper.invalidPartBOMEntriesCount > 0 %}
90-
<button class="btn btn-sm btn-outline-danger ms-2" {{ stimulus_action('pages/statistics_assembly', 'cleanup') }}>
88+
<button type="button" class="btn btn-sm btn-outline-danger ms-2" {{ stimulus_action('pages/statistics_assembly', 'cleanup', 'click') }} {{ stimulus_target('pages/statistics_assembly', 'bomButton') }}>
9189
<i class="fas fa-magic"></i> {% trans %}statistics.cleanup_assembly_bom_entries.button{% endtrans %}
9290
</button>
9391
{% endif %}
9492
</td>
9593
</tr>
94+
<tr>
95+
<td>{% trans %}statistics.invalid_assembly_preview_attachments_count{% endtrans %}</td>
96+
<td>
97+
<span {{ stimulus_target('pages/statistics_assembly', 'previewCount') }}>{{ helper.invalidAssemblyPreviewAttachmentsCount }}</span>
98+
{% if helper.invalidAssemblyPreviewAttachmentsCount > 0 %}
99+
<button type="button" class="btn btn-sm btn-outline-danger ms-2" {{ stimulus_action('pages/statistics_assembly', 'cleanupPreview', 'click') }} {{ stimulus_target('pages/statistics_assembly', 'previewButton') }}>
100+
<i class="fas fa-magic"></i> {% trans %}statistics.cleanup_assembly_preview_attachments.button{% endtrans %}
101+
</button>
102+
{% endif %}
103+
</td>
104+
</tr>
96105
</tbody>
97106
</table>
98107
</div>

0 commit comments

Comments
 (0)