{{-- Upload New Document — docs/wireframes-v12/add-upload-document.html (the card at ~lines 79-217), CR-15 §3.2. ## Shape ONE `.card`: a Reference section, a Document Details section with an "Add More" button and a repeater, a Remark Notes section, and the footer. That is the wireframe's own structure, section for section. ## This screen and bookings/documents.blade.php are NOT twins They draw the same wireframe page and they are not a create/edit pair, so recipe §9's twin rule does not apply and TwinViewParityTest is deliberately NOT extended to cover them (CR-15 §4.4). The reason is that the two screens answer different questions: the booking tab is reached FROM one booking and one person, both fixed before the page is drawn, and its Customer control is therefore a fixed display (CR-8 decision D11); this screen is reached from a listing and its Customer control is a remote select2 over every confirmed booking. Pairing them would assert that two different forms have the same fields, which they must not. What they DO share is the field names — `documents[i][customer_doc_type_id]`, `documents[i][file]`, `customer_id`, `remarks` — because both write the same rows through the same service, and this screen adds `booking_id` because it is the one thing the tab gets from its URL. ## Deliberate differences from the wireframe * **The Customer select is a remote select2** (decision D17), where v12 hardcodes six customers. The option label is the client's own format, built server-side by CustomerDocumentBookingSearchController::label(). * **The Booking Type chip is revealed by choosing a customer**, which IS the wireframe's behaviour (its `showBookingType()` does the same) — kept, where the booking tab's version is always visible because there is nothing to choose. * **The "all uploaded" message after the chip is new** (decision D11), and so is hiding the Document Details and Remark sections behind it. The wireframe has no such state because its data is mock; a real person whose booking type asks for four documents and who has all four has nothing to upload, and a form that lets them try would be a form that only the server could refuse. * **The wireframe's eleven hard-coded document types are mock data.** The real list is the CR-7 pivot, narrowed to active types mapped to the booking's booking type, minus what the selected person already holds (decision D18) and minus what the other rows have chosen (decision D12). * **The wireframe's Row 2 is not drawn as a second row.** It exists to show what a chosen file looks like, and that state is reproduced INSIDE every row instead — see partials/document-row.blade.php. * **Add More disappears at ten rows** (StoreCustomerDocumentRequest::MAX_ROWS), which the wireframe's own `addDocRow()` never limits. ## No lang file (CR-15 §0.3) Every string on this screen is written here, on the client's instruction — including the ones the client module puts on screen, which come in through one JSON `data-strings` attribute rather than living in Javascript. ## The screen's initial state is SERVER-rendered, always No `change` event fires on a freshly drawn page, so the form module never runs there. A rejected submit therefore comes back with the booking still selected, the co-applicants listed, the chip showing and the rows carrying their chosen types — all of it rendered here from `old()` and from the controller's `$selectedBooking` / `$cascade`. That is CR-5 §15.4's lesson and the reason this file is longer than the wireframe's markup. The file inputs are the one thing that cannot come back: a browser will not let a server refill a file input, so on a rejected submit the error renders and the control comes back empty. The copy says so rather than pretending otherwise. --}} @extends('layouts.app') @section('title', 'Upload New Document') @section('breadcrumbs') @endsection @section('content') @php /* * Rows re-render server-side from old input on a rejected submit — a * blade loop, not a JS restore pass, for the reason the header records. * At least one empty row is always drawn. * * **Capped at $maxRows.** old() is whatever was POSTED, and a * hand-rolled submit carrying fifty `documents[i]` rows is refused by * the validator (`max:10`) and then flashed back — so without the slice * the rejection screen would draw fifty repeater rows to tell the user * they may have ten. The server's own figure is the cap, passed in from * StoreCustomerDocumentRequest::MAX_ROWS rather than written twice. */ $rows = is_array(old('documents')) && old('documents') !== [] ? old('documents') : [[]]; $rows = array_slice($rows, 0, $maxRows, true); /* * The person the form is about: the co-applicant chosen last time, or * the main customer. Read strictly, because old() hands back whatever * was posted and that includes an array. */ $oldCustomerRaw = old('customer_id'); $oldCustomer = is_scalar($oldCustomerRaw) ? trim((string) $oldCustomerRaw) : ''; $oldCustomer = $oldCustomer !== '' && ctype_digit($oldCustomer) ? (int) $oldCustomer : null; $persons = is_array($cascade['persons'] ?? null) ? $cascade['persons'] : []; $coApplicants = array_values(array_filter($persons, static fn (array $p): bool => $p['is_co_owner'] === true)); $mainPerson = $persons[0] ?? null; $selectedPersonId = $oldCustomer ?? ($mainPerson['id'] ?? null); /* * The types this person may still be given: mapped-and-active minus the * ones they already hold. Computed here for the FIRST draw only — every * later recomputation is the client module's, from the same two lists in * the embedded payload (decision D12 also subtracts the other rows' * current picks, which only exists client-side). */ $allTypes = is_array($cascade['doc_types'] ?? null) ? $cascade['doc_types'] : []; $uploadedByPerson = is_array($cascade['uploaded'] ?? null) ? $cascade['uploaded'] : []; $uploaded = $selectedPersonId === null ? [] : ($uploadedByPerson[$selectedPersonId] ?? []); $availableTypes = []; foreach ($allTypes as $type) { if (! in_array($type['id'], $uploaded, true)) { $availableTypes[$type['id']] = $type['name']; } } /* * Decision D11. "All uploaded" is only a state once a booking has been * chosen: an empty form has no types because it has no booking, which is * a different thing and must not show the message. */ $allUploaded = $cascade !== null && $allTypes !== [] && $availableTypes === []; $selectedIsCoApplicant = $selectedPersonId !== null && $oldCustomer !== null; /* * Everything the client module can put on screen, in one attribute. The * module holds no wording of its own — the rule nrems-unit-import.js * established and nrems-booking-form.js repeats. */ $strings = [ 'search_placeholder' => 'Select Customer', 'co_applicant_placeholder' => 'Select Co-Applicant (optional)', 'type_placeholder' => 'Select Document Type', 'row_title' => 'Document', 'all_uploaded_customer' => 'All document already uploaded for customer', 'all_uploaded_co_applicant' => 'All document already uploaded for Co-Applicant', 'file_uploaded' => 'Uploaded', // CR-15.1 item E4: the client asked for an error naming the field // that was not filled, and this is where that field is named. See // the module's own header on why these two rules cannot ride // proengsoft and are the form JS's job. 'type_required' => 'Document Type is required.', 'file_required' => 'File Upload is required.', 'searching' => 'Searching…', 'no_results' => 'No matching bookings', 'input_too_short' => 'Type to search', ]; @endphp
{{-- ===== Reference ===== --}}
Reference
{{-- The remote select2 (decision D17). It renders with ONE option — the currently selected booking, or none — because select2's ajax mode fetches the rest on demand and a server-rendered list would be a query over every confirmed booking on every page load. Hand-written rather than : that component takes an `options` array and adds `data-control="select2"`, which nrems-custom.js's fallback then upgrades with NO ajax config — the plain, list-backed widget. This one is initialised by nrems-customer-document-form.js instead, so it deliberately does NOT carry that attribute; two initialisers on one element is how a select ends up with a search box that queries nothing. `required` is re-armed client-side from the data-validate-* attributes (its `exists` rule keeps the whole field off the generated rule set — see clientRuleExclusions()). --}}
{{-- add-upload-document.html:107-116. The wireframe hides this row with an inline style until a customer is chosen; the `hidden` attribute is the same thing without a style attribute in the markup, and it is what the module toggles. --}} {{-- Decision D11, verbatim wording, directly after the chip. --}}
{{-- ===== Document Details ===== --}}