DentalZaikoMaker — Full Changelog
=================================

Complete version history. The most recent releases are also listed in readme.txt.

= 1.29.3 =

* The staff screen now lists the people you have already registered above the form for adding a new one. The list used to sit below the whole form, so you had to scroll past every field to see who was there. "Edit" on a row now takes you straight down to the form.
* The login URL that staff open on their phone is shown as a QR code beside it, so they can point a camera at it instead of typing the address by hand. The code is drawn on your own computer by a library already shipped with the plugin; the address is not sent to any outside service.
* The QR code is 100 pixels. At 80 it could not be read once the address ran past about fifty characters, which is an ordinary length for a clinic's own domain.
* The staff screen has room at its right edge, and the form for adding a staff member is now as wide as the list above it.

= 1.29.2 =

* Fixed: on the staff registration screen the description of what each role can do was wrong and, on the phone, contradicted the choice directly above it. A lead can send an order to the supplier -- that is what the code does, and what the computer screen said -- but the option in the list still read "sending is done by an administrator". That wording was left over from before 2026-07-31, when a lead could only skip the approval step. A line below it, the same screen said a lead sends orders themselves.
* The wording had been pinned in place by a test. It asserted that the lead option contained the word "sending", written when sending was something a lead could not do, so the suite kept passing while the screen said the opposite of the code. The test now calls the permission checks and compares their answers against the table, rather than counting words.
* The description now follows the choice: only the selected permission is listed, with each thing it can and cannot do. Previously the same four lines appeared whatever was selected, so choosing Administrator or View only still explained what leads and staff may do.
* What each permission can do is now held in one place and read by both the computer screen and the phone. The two used to hold their own wording, which is how they came to disagree in the first place; the computer screen also only compared leads with staff, leaving administrators and view-only unexplained.
* "Role" is now called "Permission" throughout the screens.

= 1.29.1 =

* Fixed: in the staff app, tapping Receiving and then reading a code did not reach the receiving form when the item had never been registered before. The action chosen a moment earlier was carried through to the receiving form when the item was already registered, but the path taken by an unregistered item -- read the code, register the material, then record the stock -- dropped it, so registering ended at the menu of actions. Because it only happens for items that are not registered yet, it could not be reproduced with existing stock; a practice met it on its first days of use and nowhere else. The handover is now held in one place and covers every hop.
* The first setup step for materials now offers the three ways of getting them in, side by side: import a file, start from receiving in the staff app, or add a single one by hand. Importing a spreadsheet is the recommended route and is listed first. Previously the required step led only to adding one material by hand, and importing a file was demoted to an optional step further down the page, so a practice with a few hundred items was shown one-at-a-time typing first. Any of the three completes the step, as before: the test is simply whether a material exists.
* The separate optional "CSV import" step has been removed, since it now sits inside the material step; the import screen itself is unchanged and is still reachable from the menu, the stock list and the material list.
* Practices with nothing to import are no longer left at a dead end on the import screen: it now points to receiving in the staff app, where materials can be added one at a time as deliveries arrive, and to the single-material screen for adding one from a computer.
* Setting up now asks for suppliers before materials. A material cannot be registered without at least one supplier -- the server refuses it, the registration screen keeps its button disabled, and importing a file needs a supplier too -- yet the steps had long asked for materials first. It was the only step in the sequence that ran against its own dependency. Nothing was broken by it, because every screen offers to add a supplier on the spot, but a practice on its first day was sent the long way round. Practices already using the plugin are unaffected: a material cannot exist without a supplier link, so the supplier step stays complete for them.
* The supplier list now offers "Register a material for this supplier" on each row. It opens the material registration screen with that supplier already selected and placed first in priority, and the choice survives the code search in the middle of that screen. Until now the page only explained in prose that materials had to be linked from the material editing screen, and offered no way to get there.
* Fixed: the browser tab was blank on nineteen of the plugin's admin screens, so a practice with several tabs open could not tell them apart. Pages that are deliberately kept out of the left-hand menu are registered without a parent, and WordPress looks a title up through the menu, so it found none and left it empty. Five of the nineteen -- company, sites, staff, suppliers and connection -- are in the left-hand menu until setup finishes and are folded away afterwards, so they lost their tab name at the moment a practice finished setting up, which is why this went unnoticed for so long. On PHP 8.1 and above the same gap made WordPress pass an empty value to strip_tags(), writing a deprecation notice to the debug log on every visit to those pages; that stops too. The titles were already written where each page is registered, so they are now used rather than kept in a second list that would drift.
* The author is now shown as CS24 Co.,Ltd., and the author's address as https://cs24.biz/.

= 1.29.0 =

* New: a "Lead" role for staff, alongside administrator, staff and view-only. A lead is the person responsible for ordering at one site: within that site they raise, approve and send orders without waiting for an administrator, and they can also record receiving inspections and returns. Practices where the same one or two people reorder every day were making the same round trip each time: the person who knows the stock raises the request, and someone else has to be found to approve it before anything can be sent.
* A lead's authority stops at their own site, and stops short of managing people. They cannot register or edit staff, and cannot see or switch to another site. Registering staff hands out permissions, so it stays with administrators: a lead who could edit staff could make themselves an administrator.
* Sending an order to the supplier can now be done from the staff app, by administrators and leads. The confirmation screen lists everything that will go out in that one e-mail, because orders are grouped into one message per supplier: a lead sending their own single item would otherwise silently send other people's unsent orders for the same supplier as well. It also says plainly that sending cannot be undone.
* The site a member of staff belongs to now decides what a lead may touch, so it is no longer optional for them: a lead cannot be saved without one, and the staff list marks anyone who is missing one. Previously the field defaulted to unset and was stored as empty, which for a lead would have meant no limit at all rather than no access.
* A lead needs no WordPress account. They are registered on the Staff screen with a name, an e-mail address and a six-digit PIN, and sign in to the staff app with those. Because there is no WordPress account, there is no way for them to reach the WordPress administration screens at all -- the restriction is structural rather than a hidden button.
* Confirming an order is still not the same as the supplier receiving it, and every screen says which of the two has happened. What has changed is who closes the gap: a lead now finishes the job themselves rather than waiting for someone at a desktop computer.
* New: whoever raised an order is told by e-mail once it actually reaches the supplier. Until now every notification went to WordPress administrators, so a member of staff who signs in with a PIN was never told anything at all -- they saw their order confirmed on screen and had no way of learning whether it had been sent. This is the main reason "confirmed" kept being read as "delivered".
* Whenever a lead confirms an order without approval, the administrators are told by e-mail. The message names the person, lists the products and quantities, and states that the supplier has not yet been sent anything. A cart produces one e-mail for the whole cart rather than one per item; an administrator confirming their own order produces none, as before.
* The audit log now records who confirmed an order, including staff who sign in with a PIN. Until now the approver was stored as a WordPress user number, which such staff do not have, so the entry named nobody. The staff member's own identifier and name are recorded alongside, and confirmations that skipped approval are marked as such. No database change was needed.
* Fixed: a cart confirmed in the staff app by someone signed in with a PIN was left awaiting approval, even though ordering the same material one at a time confirmed it immediately. The cart asked WordPress who was signed in, which for a PIN session is nobody. It now asks the same question the rest of the app asks, so one item and a cart of ten give the same answer.
* Roles are shown in words -- Administrator, Lead, Staff, View only -- on the staff list and in the staff app, instead of the internal name printed in capitals.
* Fixed: signing in to the staff app on a phone failed on the first attempt every single time, and only went through on the second. The sign-in form carries a hidden security token that is only good for a day, but the app had been keeping its own stored copy of the sign-in page and serving that copy indefinitely, so the token it presented was always long expired. The rejected attempt is what produced a fresh token, which is why the second try always worked. The page is a rare case where a stored copy is worse than no copy at all -- signing in cannot be done offline anyway -- so it is no longer stored.
* Fixed: the home screen of the staff app was served from the same kind of stored copy, so the person's name, their role and the counts on the tiles were frozen as of the first time the app was opened on that device. On a tablet shared by the practice, the screen could show the name and role of whoever opened it first. It is now read fresh whenever the device is online, and the stored copy is used only when there is no connection.
* Internally, the list of roles and their labels now lives in one place. It had been written out separately in four, and the canonical list was referenced by none of them, so adding a role would have left it missing from a screen without any error.

= 1.28.0 =

* "Materials" is renamed "Registered dental material DB", in the left-hand menu, on the screen itself and everywhere it is linked from (the dashboard card, the supplier master, the cart, material editing and the feedback form). The address of the screen is deliberately unchanged, so bookmarks, past e-mails and links from other screens all keep working.
* That screen and the stock list are now paged. The stock list shows fifty rows to begin with and offers twenty, a hundred, or all of them; the material screen shows twenty. Both keep the search, the category and the other filters when moving between pages, and both state how many rows the filter matched rather than how many are on the page.
* On the stock list, ticking rows for the cart only carries within one page, because the ticks are one submission. Rather than let a selection disappear without a word, the button reads "Add the ticked rows on this page to the cart" whenever there is more than one page, and the screen says the ticks are lost on moving. Choosing to show all rows brings back the old behaviour and the old wording.
* Fixed: the material screen only ever fetched five hundred rows. A practice with more could not find the rest at all -- not by searching, not by filtering -- and nothing on the screen said so. The limit is gone.
* Fixed, on the feedback form: reaching it without a material told the reader to use a "Post" button on the used-materials list. That button moved to the "Feedback to makers" screen in this release, so following the instruction led nowhere. It now points at the right screen.
* The business type (dental clinic or dental laboratory) is now held by the web service and copied into this plugin every time the site authenticates. The same value drove the "type" line of order e-mails, the orders sent to a linked seller and the monthly figures, but it was chosen here while the web service kept its own answer, and the two were never reconciled. The field on the corporate details screen still shows the current value, so a practice can see what it is, but it can no longer be edited; the screen explains that a change is requested from the web service. A site that has not connected yet keeps choosing its own, and disconnecting hands the choice back.
* The "Send Web order to this seller" button on the ordering screen is now green and pulses. It is the last step before a linked seller sees the order at all, but it was drawn in the same quiet dark blue as everything around it. Green is used rather than the red of the e-mail route, so the two ways of sending stay distinguishable at a glance.
* Prices are shown only where a price is actually on file. A material with no purchase price recorded is held as zero, and the ordering screen printed that as "@¥0" beside each line of a Web order, which reads as "ordered for nothing". The same was true of the supplier candidates shown before a material is added to the cart, on a phone and when registering a new material on a computer. Where the price is unknown, nothing is printed rather than a zero.
* New: a shopping cart for ordering. Materials can be collected into a cart and ordered together, instead of repeating the same form once per item. Ordering one item at a time still works exactly as before.
* The stock list, on a computer and in the staff app, gains a checkbox on each row and an "Add selected to cart" button, so a week's worth of reordering can be gathered in one pass. Items with no supplier on file cannot be ticked, because an order could not be sent for them.
* The "Add selected to cart" button now stays at the foot of the screen while the stock list is scrolled, instead of sitting only at the very end of it. A practice with several hundred materials had to scroll past all of them to reach the button. The list is deliberately not split into pages: the ticks belong to one form covering the whole list, so paging would quietly drop everything ticked on the page you left.
* That button turns red and pulses once at least one material is ticked, and returns to being plain and unavailable when the last tick is removed.
* The stock my-page on a phone now opens with "Order" as the first tile, followed by the cart and, for an administrator, awaiting approval. Until now the only way in was the small "N to reorder" chip at the top of the screen: the operation used most often every day was the smallest and palest thing on the page, while receiving -- used less -- was the largest and darkest. The chip has been removed rather than kept alongside the tile, so that one destination still has exactly one control. Receiving keeps its dark blue: the "Order" tile turns red only on the days when something is actually below its reorder point, so it never tells a practice to order on a day when nothing needs ordering. The cart and approval tiles appear only when they hold something.
* The list of materials below their reorder point gains the same "Add selected to cart" button as the stock list, so a morning's reordering no longer means opening one form per item. The quantity carried into the cart is the shortfall (reorder point minus stock on hand) rather than a flat one, and pressing the button twice no longer doubles it.
* Fixed: rejecting an order left its material marked "already ordered". That mark is set the moment an order is raised, and until now only receiving the goods could clear it, so a rejected material stayed out of the reorder list and out of the daily low-stock e-mail for up to thirty days even though the stock was still short. Rejecting now clears it, unless another order for the same material is still awaiting approval or has been sent and not yet received.
* Fixed: the stock list showed a red "reorder" button on materials already marked as ordered, next to the "ordered" badge on the same row, and disagreed with the count shown on the my-page. The red prompt is gone for those materials; their position in the list and the plain "Order" button are unchanged, so a material that is short but already on order can still be seen and ordered again on purpose.
* The "already ordered" mark can now be cleared by hand from the stock list. It could be set from two places -- the reorder list and the low-stock e-mail, which needs no login and so can be pressed by anyone -- but there was no way at all to undo it.
* The cart groups its contents by supplier, shows a subtotal per supplier and how each one will be reached (web order / e-mail / EC site / no destination on file), and lets the quantity and the supplier be changed before ordering. A supplier with no destination is called out before the order is placed, not after.
* Confirming a cart creates one ordinary order request per line, so approval, sending, expense figures and the optional statistics report work exactly as they did. Requests made from the same cart are shown together on the ordering screen and can be approved in one action; approving or declining them individually is still possible.
* A cart sent for approval produces a single e-mail listing every item, rather than one e-mail per item.
* A cart holds up to 50 items, and belongs to the person who filled it: no one else's cart is shown or can be altered.
* Fixed: the cart could show the same supplier as several separate cards -- one per material -- each with its own delivery date and note, so the same information had to be typed twice for a single order. The cart grouped by the supplier master, and fell back to the per-material link row for any supplier that had none, and that link row has a different id for every material. Materials for the same supplier are now kept together whatever they are linked to, with one subtotal, one delivery date and one note.
* The delivery date and the note now sit inside the supplier's own card, next to the materials they apply to, instead of in a separate list below the cart.
* Where one supplier is reached in more than one way -- some of its materials through its web shop, others with no destination on file -- the card says so and each material carries its own label, and the warning shown before ordering now takes every material into account rather than the first one.
* A supplier that is not in the supplier master can now be registered from the cart, either as a new supplier or by pointing it at an existing one, and every material naming that supplier can be moved across in the same action. This is what stopped orders reaching such a supplier by e-mail: the order e-mail is addressed from the supplier master, so a supplier that was not in it could never be written to, whatever address was on file elsewhere.
* The default supplier on the data-import screen is now chosen from the supplier master, instead of being typed in by hand. Whatever was typed became a company of its own in the master, and the master can only be deactivated, never deleted, so a single mistyped name stayed on the list for good. Each entry in the list also shows how that supplier is reached, so a supplier with no destination on file can be spotted before the import rather than when the first order fails to arrive. A supplier that is not on the list can still be added from the same screen, but it is only written to the master when the import is actually carried out, so abandoning a preview no longer leaves anything behind.
* A supplier name in the file that matches one in the supplier master is now filed under the master's own spelling. "Smith Trading" and "Smith  Trading" went in as two separate companies; they are now recognised as one. Names that match nothing are still registered as new, and the preview marks each such row, so they can be checked before the import goes ahead.
* Fixed: a unit price written with a decimal, such as 1,234.50, was read as 123450. Every digit was kept and the decimal point discarded, so the expense figures and the purchase-price trend for that import came out a hundred times too large. Prices written with a currency symbol, in full-width digits, or as a range are also handled: a range is left blank rather than turned into an impossible number, and a blank or zero price is left unset rather than recorded as free of charge.
* Column headings for a unit price are recognised in more spellings. Headings that mean a line total rather than a price for one item are deliberately not recognised, because the total is worked out from the price and the quantity and would otherwise be multiplied by the quantity twice.
* The import preview now shows the unit price and the supplier for every row, and says when the same product appears at more than one price -- which is normal in a dealer's sales history, and is why nothing is overwritten: every purchase is kept as its own line with its own date. The price recorded against the material for reference is the one from the most recent purchase date, so a file listed oldest-first no longer leaves the oldest price attached to the material.
* A material created by an import now carries a reference price, so a spreadsheet with prices in it no longer produces materials whose cart shows no amount at all.
* The preview also states how many rows carry a price that will not count towards expenses because they are being brought in as opening stock, rather than dropping those prices silently.
* Fixed: a purchase history brought in from a spreadsheet was counted as an order still waiting to be sent. Importing a couple of hundred past purchases put that number on the ordering badge and listed every one of them under "no destination on file", although they had been bought and paid for long before. No order e-mail was ever sent for them, but the screen said otherwise.
* Fixed: a material with an imported purchase history stayed marked as "already ordered" for good, because the imported rows counted as deliveries still to come. Its low-stock e-mail therefore stayed switched off even when the stock ran out.
* Importing stock from a spreadsheet now registers each supplier name in the supplier master instead of leaving the material attached to a name alone. Imported materials could be ordered on screen but the order was never e-mailed. The import preview lists the suppliers that will be newly registered, so a name spelled two ways can be corrected before it becomes two suppliers.
* The order e-mail now states where the goods are to be delivered. It carried the corporation's address only, so an order raised for a branch surgery still showed the head office address, and an e-mail covering more than one site gave the supplier no way to tell which box went where. Each site's own postcode, address and telephone number are listed, and when one e-mail covers several sites each ordered line names its destination and each site shows how many lines are for it. A site with no address or telephone on file is called out rather than left blank, so it is clear that the detail is missing rather than absent.
* New: the customer number a supplier has assigned to the practice can be recorded against that supplier, and is quoted in the order as "your customer code". Suppliers identify an account by their own number; a practice name alone leaves the person keying the order to work out which of several similarly named practices, or which branch, it belongs to. The field is optional -- practices often do not know the number -- and nothing is written in the order when it is blank. It is quoted on both routes: an e-mailed order and an order placed through a linked distributor.
* An order sent to a linked distributor through the web service now carries the same detail as an e-mailed order: where the goods go (each site's postcode, address and telephone), the note and the wanted delivery date, and who is ordering. The note and the delivery date were being typed by the practice but never reached a distributor on this route at all.
* The order e-mail also states whether the sender is a dental clinic or a dental laboratory.
* The "E-mail order" label in the supplier master now has a pale yellow background. It was the only one of the three ordering methods drawn in the default grey, so beside the blue "Web order" and "EC site" labels it read as plain text rather than a label.
* The row of buttons for the six states of an order -- all, awaiting approval, unsent, awaiting receipt, approved, declined -- now appears on the "Awaiting approval" and "Receiving inspection" screens as well, with the current one filled in dark blue. Two of the six lead to those screens, because approving and inspecting are separate jobs from ordering; but once there, the row was gone, so moving between them meant going back through the ordering screen first, and nothing on the page said which of the six you were looking at.
* The ordering screen groups "cannot be sent automatically" orders by supplier in the same way as the cart, so a supplier gathered into one card there is no longer split back into one box per material. EC purchases and orders with no destination stay in separate boxes, because the button records a different thing in each case.
* New: the low-stock e-mail can now be acted on from the e-mail itself. It repeats every day until stock returns, and until now the only ways to stop it were to place the order through the plugin, to lower the threshold, or to move every notification to weekly. A clinic that orders by telephone or fax had no way at all, which is why the same message arrived every morning.
* The link at the foot of that e-mail needs no login, because the notification address is not necessarily a WordPress account. Opening it shows a page with the material's name and three choices: mark it as already ordered, silence its e-mail for 30 or 90 days, or silence it indefinitely. Marking it as ordered is offered first and is the safest: it clears itself as soon as the delivery is recorded, and returns on its own after thirty days if nothing arrives.
* Silencing stops the e-mail only. The material still appears in the notification list and in the stock list, marked as silenced, because a shortage that has become invisible is a shortage nobody acts on. A timed silence lifts itself on the day it expires.
* Silenced materials are listed on the Notifications screen with the date they return, and can be restored there in one click. The same setting is on the material screen, on a computer and in the staff app.
* Because the notification address can be shared, silencing a material affects everyone at the clinic. The confirmation page says so, and a record is written to the notification list -- on screen only, never by e-mail -- so that a colleague who did not press it can see what happened and why the alerts stopped.
* Opening the link does not change anything by itself. Mail scanners and link previewers fetch addresses in messages without a person involved, so the link only ever shows a page; the change happens when the button on that page is pressed. The address is signed and stops working after sixty days, and the signature covers the material, so one message cannot be edited to silence a different one.
* New: notifications can be deleted. The notification list had no way to remove anything: a shortage notice is raised for every material below its reorder point every day, so the list grows by several rows a day and the things that need acting on -- an order awaiting approval, a lot about to expire -- sink out of sight. Each row now has a delete link, the rows ticked on a page can be deleted together, and every read notification matching the current filter can be cleared in one action. Unread notifications are never removed by either bulk action, because a notification nobody has looked at is a job nobody has done.
* Deleting hides the notification from the list; the row itself is kept. This is not tidiness for its own sake: the daily check decides whether it has already told you about something by looking for the earlier notification, so if the rows were really removed, an expiry warning would be raised again the next day and its e-mail sent again. A deleted notification stays deleted.
* Deleting a notification is not a way to stop being told. The shortage itself is unchanged, so the material stays in the stock list and the reorder list, and tomorrow's check raises the notice again. The screen says so, and points at the link in the e-mail for silencing a particular material instead.
* The ticked-rows button says "delete the rows ticked on this page" rather than "delete the ticked rows", because the list is shown twenty at a time and moving to another page does not keep the ticks. Clearing read notifications does not use the ticks at all, so a long list can be cleared in one press whatever page it is on. That button is hidden while the list is filtered to unread only, where no read notification is visible to judge.
* New: notifications older than a set number of days are cleared during the daily check -- 180 days to begin with, or none at all if the number is set to zero. Only read notifications and ones deleted from the list are removed; unread notifications are kept however old they are. Numbers below sixty days are raised to sixty, because clearing a notification the daily check still needs to see would make the same notice, and its e-mail, arrive all over again. At most five hundred are removed in one run.
* Fixed: the notification list showed the delivery-channel and severity badges but offered nothing to do with a notification once it had been read; the row simply stayed for good. Reading a notification and clearing it are now separate actions on the same row.
* Fixed: a confirmation asked twice on screens where something is deleted or stopped -- the browser's own confirmation box and the plugin's dialog both appeared, one on top of the other. This affected deleting a budget, stopping a site, deactivating a supplier and unlinking a seller as well as the new delete links.
* Fixed: returns could not be recorded from the staff app at all. The button was there, the form was there and the code that saves the return was there, but the address it pointed at was not registered, so tapping "Return" landed on the site's "page not found". Returns from a phone or tablet now work.
* A return recorded in the staff app can now be tied to the order it came from, and its unit price entered, exactly as on a computer. Without the order there is no supplier attached, so the return request could not be sent on to a linked seller — a return from a phone stayed a note to yourself.
* Receiving stock in the staff app now asks which supplier the goods came from, as the computer screen already did. Without it, deliveries recorded on a phone were left out of the spend-by-company figures and the price history.
* The stock list in the staff app now has a "Stock operations" link on each item. Receiving, use, disposal and returns could previously only be reached by scanning the barcode, so an item whose code would not read could not be handled from the list.
* The stock operations in the staff app — receiving, use and disposal — are now separate buttons on the home screen, each taking you straight to that operation after a scan, instead of one "Register stock" button that reached them two screens deep.
* Fixed: on a computer, a material that the master database does not hold could not be registered, although the screen said it could. Step 2 of the registration form only appeared when the search found something. It now appears whenever a search has been made, so gowns, paper cups and anything else outside the medical-device database can be registered and received — as the staff app already allowed.
* Fixed: the expense dashboard and the branch comparison ran off the side of a phone screen. Their two-column layout did not fold, so the right-hand charts sat outside the display. The supplier master and the staff list did the same, and the supplier names were squeezed to one character per line.
* The lot chooser now reads the same on a computer and in the staff app, and it, the volume-discount note, the budget scope labels and the "unclassified" series in the price chart are translatable; they were previously fixed Japanese.
* Order approval is now gathered into one "Needs approval" screen that works on both a computer and the staff app, so an administrator on a phone can approve as well; sending to the supplier is still done on the computer.
* Security: the supplier recorded against a receipt or a return is now checked against the material it belongs to, instead of being trusted as sent. Marking an order received is likewise tied to the site and material it was ordered for.
* New: receiving inspection. Orders sent to a supplier now appear in an "Awaiting receipt" list on the ordering screen and in the staff app, so a delivery can be checked against the order it belongs to. Receiving stock used to be disconnected from the order — there was no way to see which orders were still outstanding, or to compare what arrived with what was ordered.
* When receiving against an order, the received quantity defaults to the quantity ordered and can be changed; a difference is recorded as a shortage, an over-delivery or damage, with a note. The order is then marked received and leaves the list.
* New: storage locations. Each site can register where materials are kept (a fridge, a cupboard, a shelf) on a new "Storage locations" screen, and choose one with a button when receiving stock. Four shelves ("棚1"–"棚4") are set up to begin with and can be renamed, added to or retired. The same lot kept in two places is now tracked as two separate stocks, and the stock list shows where each lot is. Receiving without choosing a location works exactly as before.
* When scanning to receive, a code is matched as a JAN barcode first and, if nothing is found, against a house (in-store) code. If a single code matches one material by JAN and a different material by house code, both are offered with a "Match by house code" button, rather than silently guessing.
* Ordinary receiving that is not tied to an order still works exactly as before, on a computer and in the staff app.
* New: a product photo can be taken from the stock list in the staff app. Items with no picture now show a camera button where the thumbnail would be; tapping it opens the phone camera and the photo is attached to that item straight away, without leaving the list. Photographing stock previously meant going to a computer, so most items stayed as a plain box icon and were hard to tell apart on a shelf.
* New: an "Edit" link at the bottom of the left-hand column of each row in the stock list, for correcting the product name, maker, category, unit and reorder point, and for replacing or removing the picture. Until now a name typed in wrongly, or a unit entered as "10 boxes", could only be fixed from a computer.
* The JAN and house codes are deliberately not editable there, and the screen says so: they are what a scanned barcode is matched against, and changing one would leave an item that scans but is never found. Renaming an item does not rewrite past orders, which keep the name they were placed under.
* The stock list can be filtered by the 19 catalogue categories, chosen with the same buttons as the search.
* Stock operations on the staff app are split into separate receive, use and dispose buttons; the chosen operation carries through to scanning, so a scanned code goes straight to that form.
* Approving orders is gathered into one "Awaiting approval" screen, reachable on a computer and, for a signed-in WordPress administrator, on a phone. A whole cart can be approved in one action.
* A wanted delivery date and a note to the supplier can be entered per supplier when a cart is confirmed, and both appear in the order e-mail.
* The order e-mail now carries the date and time of the order, the JAN and in-store codes of each item, and where the order can be checked on the web service (a link to obtain a linking code, or to sign in, depending on whether the supplier is linked).
* The notice that an order e-mail has been sent now names the supplier as well as the address, and the send button is marked as the action to take.
* The result of an order is shown as a message that cannot be missed, once per action rather than repeated on every screen: an order raised by staff says an administrator has to confirm it and links to the approval list; a confirmed order says it is confirmed but not yet sent; a sent order says it has been sent.
* Suppliers a material can be ordered from are now chosen from the supplier master instead of being typed in by hand -- on the material screen, in the staff app, and when registering a new material. A supplier typed in by hand could not hold a destination, so an order placed against it could not reach anyone; it was a dead end from the moment it was added. A supplier missing from the list can be added on the spot, and a supplier that was typed in by hand before can be pointed at a master record.
* Every supplier must now have somewhere an order can be sent, decided when the supplier is saved: an e-mail address, or, for a shop that sells only through its own web shop, the address of that shop. Ticking "Bought from an EC site (no e-mail address needed)" on the supplier master swaps the field that is required from the e-mail to the URL. Until now such a shop had to be given an e-mail address to be saved at all, and an order was then sent to a counter that does not accept one. A supplier registered through seller linking keeps its web order route, and cannot be changed to either method from this screen.
* The supplier master list gains an "Order method" column -- by e-mail, from an EC site, or a web order -- and marks a supplier whose method has no destination on file.
* Orders to suppliers this screen cannot send to -- an EC site, or no destination on file -- now show the order itself on the ordering screen: every item with its quantity, its JAN and in-store codes, and the wanted delivery date and the note entered in the cart, with a button to copy the list for pasting into the shop's own basket. The delivery date and the note previously appeared only in an order e-mail, so for these suppliers they were recorded and then shown to nobody.
* Once the goods have been bought, "Bought on the EC site (record it)" marks the order as placed and moves it on to receiving inspection, where the price actually paid is entered. Nothing is sent anywhere; the button records what the clinic did. In the staff app the same order shows the same list and says the purchase is recorded on a computer.
* An EC supplier with no address on file is offered a "Register the URL" button on the ordering screen, rather than being quietly treated as a supplier to be e-mailed.
* Where a supplier has no destination on file, the screens that say so now also link to where it can be set.
* The note beside a supplier in the ordering choices now says which destination it has -- an EC site, a web order, or none on file -- instead of reporting a shop that sells only through its own web shop as missing its e-mail address.
* Fixed: a supplier saved from the staff app could be left with no destination at all, neither an e-mail address nor a site, because only the screen on a computer checked. An order placed against such a supplier could not reach anyone.
* Fixed: updating a supplier from the staff app erased its branch name and its corporate number, because the form carries neither field. Where two branches of the same company were registered, the emptied branch name then made the supplier look like a duplicate of the other one and it could not be saved at all. Both fields can now also be corrected from the staff app.
* Fixed: the supplier master list showed every active supplier as inactive and offered no way to deactivate one.
* Fixed: registering a material from the staff app failed with "register at least one supplier" whenever an existing supplier was chosen from the list; only typing a new supplier name worked.
* Fixed: an error while adding a supplier to a material was recorded but never shown, so the screen simply returned as if nothing had happened.
* Security: refused operations in the staff app answer 403 instead of a server error, and receiving an order is bound to the site and material it belongs to.
* Fixed: on a computer the category of a material was still a free-text box, so a typo left the material out of the category filter. Categories are now chosen with the same buttons everywhere -- editing a material and registering one, on a computer and in the staff app. Opening and saving a screen no longer changes a category that was not touched.
* The supplier a material is ordered from is now part of the basic information form, showing the supplier on file and letting it be changed from the supplier master in the same save. The two separate blocks lower down the screen are gone; adding and editing suppliers themselves is done in the supplier master.
* Receiving inspection and awaiting approval each have their own screen. Both used to open the ordering screen with a tab preselected, so the heading said "Ordering" and the "Create an order" button sat at the top, which read as being sent to the wrong place. Neither screen now offers anything to do with creating an order, and the ordering screen links to them instead.
* Fixed: after recording a receipt the screen returned to the ordering list rather than to receiving inspection, so it was not possible to see that the line had gone.
* The awaiting-receipt list is grouped by site, with a heading and a count for each, and shows a photograph of each material so that what arrived can be compared with what was ordered.
* The left-hand menu is reduced from twenty-six entries to fourteen. Nine settings -- corporation, sites, staff, storage locations, supplier master, seller linking, connection, data import and the public report -- are gathered behind one "Settings" entry, and five analysis screens behind "Cost analysis". Each of those screens gains a row of buttons to move between the others. Addresses are unchanged, so existing links and bookmarks still work, and during first-time setup the settings stay in the menu so the guided steps still lead to them.
* The "Settings" entry shows how many settings still need attention, and the settings page lists each one with its current state. Only what can actually be cleared is counted: an unfinished setup step, an undecided answer about the monthly report, or a supplier with no e-mail address (an order cannot reach such a supplier). Seller linking, storage locations, data import and the monthly budget are shown but not counted, because leaving them alone is a legitimate choice.
* Fixed: opening a screen that is not in the menu -- editing a material, stock operations, the consent screen -- left the plugin's menu unhighlighted, so there was nothing on screen to say where you were.
* The unit price asked for when receiving stock is outlined in red on all four screens that record a receipt, and leaving it empty now says "Register the purchase price when receiving stock." A missing price throws out both the expense figures and the price statistics.
* Required fields that are easy to get wrong now say what to do instead of showing the browser's own wording: which lot to take stock from, why something is being returned, which supplier to order from or to add, the linking code from a seller, the e-mail address an order is sent to, and the PIN used to sign in. Fields whose label already says everything are left as they were.
* A material can be opened from the shopping cart and the cart from a material, so correcting a material and adding to the cart no longer needs the browser's back button.
* Fixed: two screens checked whether the practice had agreed to the external service, but not whether the site was actually connected to it. On a site that had agreed without yet saving and testing its connection key, no warning was shown and the only response was "Corporation ID or API token is not set" -- wording that says nothing about what to do. Both screens now name the step that is missing and link straight to it. On the seller-linking screen the "Link" button stays disabled until the site is ready, and a form posted directly is refused with the same wording. When registering a material the search is deliberately left enabled: a material can still be entered by hand in step 2, and the notice says so. The same check now looks at whether the connection key can actually be read, not merely at whether a connection once succeeded, so a site whose stored key can no longer be decrypted -- after a move to another server, or after security keys were regenerated -- is told to paste its connection key again instead of being shown the technical message. The recorded "connected" state is cleared at the same time, so the settings screen and this notice no longer contradict each other.
* Fixed: searching the master database when registering a material refused a GS1 code -- the fourteen-digit number printed on medicines and on many outer cartons -- because the field accepted at most thirteen digits and the search itself was only ever asked about a JAN code. Both are now accepted, in the staff app as well as on a computer: a fourteen-digit code is looked up as a packaging GTIN, and where the code carries a JAN inside it (an indicator digit of zero, so the last thirteen digits are the JAN) that JAN is recorded against the material and the full code is remembered, so scanning the same carton later finds it at once. A fourteen-digit code is never stored as the material's JAN, since a JAN of that length would be refused by the web service when an order, a return or a report about the material is sent, leaving a material that could be registered but never ordered. The scanning screen already accepted these codes; its wording said otherwise and has been corrected.
* Fixed: on the material registration screen, the box for a supplier's reference price was only wide enough for four digits, so a five- or six-figure amount was cut off and could not be read back. It is now wide enough for the amounts actually entered. Every other number box on a computer had the same fault, since all of them were the same width: how long notifications are kept showed "180" as "18C", and a quantity, a reorder point or a supplier's priority of three digits ran under the up-and-down arrows in the same way. All of them are now wide enough for five digits, and the price box stays wider still.
* Fixed: searching the stock list or the material list for a GS1 code found nothing, even for a material that was registered from that very code. The same product has two forms -- a thirteen-digit JAN and a fourteen-digit GS1 beginning with zero -- and only one of them is stored, so searching in the other form matched nothing at all. Both forms are now tried, and the code remembered from a scan is searched as well. A keyword that contains any letter is treated exactly as before, so ordinary searching does not become vaguer.
* Fixed: importing a stock list whose JAN column holds a fourteen-digit GS1 code stored that code as the material's JAN, which meant that orders, returns and reports about the material could never be sent. Such a code is now split the same way as on the registration screen.
* After searching the master database when registering a material, the screen moves down to step 2, where the details are entered. It stays where it is when nothing was found, so the explanation of why -- and, in the staff app, the offer to read the barcode again -- is not scrolled past.
* On the reorder list of the stock page, the button that opens "silence this material / change the reorder point" is now a small plain button instead of a large green one, and the separator line sits below it. That control is used occasionally; the strongest colour on the screen belongs to "add the selected items to the cart", which is used every day. That cart bar now has a tinted background, a heavier border and a full-width button, so it reads as the main action rather than the weakest thing on the page. With nothing selected the button is grey and does not pulse.
* The list of materials below their reorder point now carries the quantity and the supplier on each row, so a whole morning of ordering can be settled in one pass. Until now the only way to change either was the red "reorder" button, which opens the form for that single material and does not come back to the list -- so a practice with eight materials to order placed eight separate orders, and the cart could not be used for what it was built for. The quantity starts at the shortfall (reorder point minus stock on hand) and can be edited in place; where a material has more than one supplier on file, one can be picked on the row, and where it has only one that supplier is named. Ticking the rows and pressing "add the selected items to the cart" carries each row's own quantity and supplier. The red "reorder" button on each row is now a plain "Change" button: with the quantity and the supplier on the row there is nothing left for it to do on an ordinary day, and leaving the strongest colour on the screen pointing at the one-material-at-a-time route was the reason the cart went unused. It still opens the full ordering screen, for choosing between suppliers with their prices and stock in view. The red "reorder" button on the stock list is unchanged. A supplier that does not belong to the material is refused, whatever is submitted.
* Fixed: on a phone, the "add the selected items to the cart" bar did not stay at the foot of the stock page -- it could only be reached by scrolling to the very end of the list, which is exactly what the bar was added to avoid. The bar is positioned to stick to the bottom of the screen, and that silently stops working when any ancestor element is a scrolling box; a theme only has to hide horizontal overflow on one of its wrappers for it to happen, because hiding one axis forces the other to scroll. The stock my-page is rendered inside a theme page, so the ancestors belong to the theme and no stylesheet of ours can reach them. The page now checks whether that is the case and, only then, pins the bar to the screen instead, keeping the height of it free at the end of the list so the last row is not covered. Themes that were already working are left exactly as they were.
* On a phone, the cart now keeps its total and the "order what is in the cart" button at the foot of the screen while the contents are scrolled, instead of only at the very end of the page. The whole panel is kept together on purpose: showing the button without the total would let an order be confirmed without seeing what it comes to. The two buttons sit on one line -- "Empty" on the left, kept narrow and plain because it cannot be undone, and the ordering button on the right taking the rest of the width. Both labels were shortened so they fit, and the ordering button now says what actually happens: "Request order" for a member of staff, whose order goes to the director for approval, and "Confirm order" for an administrator, whose order is settled on the spot. It matches the sentence directly above it. The order button is red and pulses, and it is the only pulsing thing on the screen.
* Fixed: the cart showed "0 yen" for materials whose purchase price has not been recorded yet, which reads as if the order were free. A price that is not known cannot be added up, and treating it as zero was wrong in both directions: a cart holding only newly registered materials came to nothing at all, and a cart with one such material quietly came to less than it will cost. Where no price at all is known the amount is now shown as a dash rather than a figure; where some are known it is shown as "at least" that figure, and a line underneath says how many items were left out and that recording the purchase price when the goods arrive will include them next time. This applies per supplier and to the whole cart, on a computer and in the staff app, from one place so the two cannot disagree.
* Database update runs automatically on upgrade. DB schema 1.13.0 -> 1.16.0 (receiving inspection and storage locations; per-material silencing of the low-stock e-mail; two columns on the notification table so a notification can be removed from the list without losing the record that it was already sent).

= 1.27.0 =

* New: returns. Stock sent back to a supplier can now be recorded as a return instead of being written off as waste. Waste is a loss; a return gets the money back, so recording one as the other made the expense figures wrong.
* The return amount is deducted from the expense dashboard, the accounting CSV exports and the optional monthly statistics report. Until now there was no way to record a return at all, so those figures counted the full order even after goods went back.
* The expense chart shows returns as a separate line, so a drop in spending can be told apart from goods being returned.
* Returns are limited to administrators. Recording one moves money, so it is treated more carefully than use or disposal.
* If the goods were ordered through a linked seller on the web service, recording the return also sends a return request to that seller, who accepts or declines it in their own page. The decision comes back to the clinic. Returns of goods bought any other way stay entirely on your own site.
* A return is also a record of what went wrong with a material. Choosing "defective" as the reason keeps that fact on file, and the material's own screen lists its return history, so a material that keeps arriving faulty can be spotted. Nothing about a return is sent to the manufacturer: whether to report a defect is the clinic's decision, made from the existing feedback screen.
* Both the return screen and the feedback screen now state that neither a record here nor a post to the manufacturer takes the place of the report to the PMDA required of dentists by article 68-10(2) of the Pharmaceuticals and Medical Devices Act, and link to it.
* Corrected: the feedback screen said an urgent defect report "may be" passed to the dental association and the dental technicians' association. Those arrangements are not in place, so the report goes to this plugin's web service and the manufacturer only. The screen now says so.
* Accounting exports now include return rows: the transaction detail CSV marks each row as an order or a return and shows returns as negative quantities and amounts, and the freee / MoneyForward exports reverse the entry.
* The "dead stock" and "overstock" suggestions now link straight to the screen where the goods can be disposed of or returned. They previously suggested returning goods when no such operation existed.
* Fixed: the stock history showed a raw "initial" for rows created by the data import, instead of a readable label.
* The signature at the foot of every e-mail the plugin sends now separates the two things it was mixing: your own clinic or laboratory as the sender, and the system the mail was produced by.
* The organisation screen now says what its name and e-mail address are actually used for, that they are separate from the account held by the web service, and that changing them does not reach orders already sent.
* First-run setup has been rebuilt around one trail to follow. Until the required setup is done, the plugin's top page is the setup screen itself rather than a wall of feature cards, and there is exactly one blinking red button on any screen — following it always lands on the button that actually does the work, right down to the Save button on the form. Completed steps collapse to a single line and the numbered list separates what is required from what can wait. Green means done, red means act here, grey means not yet; each also carries its own word, so the state does not rely on colour.
* Setup asks for less. The organisation form now arrives filled in from the site's own settings (name, e-mail, representative) and can create the main branch from the same details in one save, instead of asking for the same address twice.
* Staff registration is no longer required to finish setup. A WordPress administrator can already run everything from the admin screens, so the step is now a choice: register the signed-in administrator as a staff member with one click, or say that no staff app is needed. Registering staff is still there for anyone who uses the staff app on a phone.
* Fixed: the connection step counted as done once the key was merely saved, so a mistyped key was carried past setup and only failed later when a material was looked up. It now counts as done when the connection test succeeds. The connection screen also no longer keeps a second, separately-judged copy of the setup steps.
* Fixed: if a linked seller stopped accepting orders through the web service, orders addressed to them silently stayed unsent — they were no longer eligible for the e-mail route either, and the web route refused them. The plugin now checks the state of each linked seller hourly, tells you when one stops or resumes, and offers the choice on the ordering screen: send this order by e-mail instead, or wait for the seller to resume. The route is never switched for you, because an order sent by e-mail does not appear in the seller's own page, brings no automatic confirmation, and cannot be used to request a return.
* Fixed: an approved order could be left unsent with nothing on screen to say so. The two sending panels only listed orders whose supplier was linked to a supplier master record with an e-mail address, so an order placed against a web-listed company added from the staff app, or against a supplier with no contact details, disappeared after approval while still showing as approved. Every unsent order is now listed, with how it is meant to reach the supplier, and the ones that cannot be sent automatically offer the product page or the supplier record to fix. Marking such an order as handled records that fact only; it sends nothing.
* Ordering has been rebuilt around one visible sequence — create, approve, send to the supplier, receive — shown on the ordering screen and on the order form, with which of the two devices each step can be done on. Creating an order works on both a phone and a computer; approving and sending are on the computer's admin screens only. The screens used to say an order needed approval even when it did not, and did not say that approving an order does not send it.
* The "order requests" menu entry is now "orders" and is the starting point for creating one as well: it carries a button through to the stock list, a tab for orders that have not been sent yet, and a count of orders waiting for approval next to the menu entry.
* An administrator placing an order from a computer now confirms it in one step, as has always been the case from the staff app, instead of raising a request and then approving their own request. Staff orders still go to the administrator for approval, and an administrator whose staff role is set to anything other than administrator still cannot approve.
* The supplier list on the order form now says how each supplier will be reached — web order, e-mail, buy on their site, or no contact details on file — before one is chosen.
* A material with no supplier registered used to show no order form at all and no explanation. It now says why and links to where a supplier can be added.

= 1.26.1 =

* Data import now accepts Excel files (.xlsx) directly, so the workbook from a clinic or a dealer can be uploaded as it is — no more saving it as CSV first. This also removes the problems that only happen with CSV: garbled Japanese text, JAN codes losing a leading zero, and long numbers turning into scientific notation. CSV files are still accepted.
* The importer now copes with the shapes real paperwork arrives in: it finds the header row even when a title or notes sit above it, looks through every sheet instead of only the first, fills in vertically merged cells, and leaves out the trailing "total" and note rows (the number left out is shown on screen).
* Dates written as "2027/08/00" (month with no day) are read as the end of that month, and Excel date cells are converted correctly.
* The downloadable template is now an Excel file. JAN codes, supplier codes and dates are held as text so Excel does not reformat them.
* Fixed: a row whose JAN code matched but whose quantity was missing lost the matched product name. The matched name is now kept and shown alongside the reason the row cannot be imported.

= 1.26.0 =

* Saving the connection settings now runs the connection test in the same step, so the status card reflects the result immediately instead of asking for a separate test.
* Notification e-mails can be sent to several addresses (one per line or comma-separated), and the daily check can be switched between daily and weekly.
* The staff PWA login is now rate-limited: repeated failures from the same address or IP are locked out for a short period. Newly set or changed PINs must be six digits; existing shorter PINs can still sign in.
* Security: the API token is no longer read from the plaintext pending option after setup — any leftover plaintext is migrated to encrypted storage on use. The audit log no longer stores the PIN hash. When AUTH_KEY is undefined, a site-specific random salt is generated for token encryption instead of a shared constant.
* Camera scanning now prompts to type the code in by hand when nothing is read for a while, since some small packages carry a barcode the bundled decoder cannot read.
* CSV import recognises a "supplier product code" column, so materials keyed by a supplier's own code rather than a JAN code are matched to the same material on re-import.

= 1.25.0 =

* Merges the WordPress.org review work (1.21.6) with the features released to clinics in 1.22.x–1.24.0, so both lines are one code base again.
* Supplier records now carry a corporate number (13 digits) and an office name. Entering a corporate number looks up the companion web service and links a registered distributor automatically.
* The daily low-stock e-mail now carries a link to the reorder list, and ordering marks the material as "on order" so the e-mail stops until the stock is received (it is released automatically on stock-in, or after 30 days).
* The reorder list can pause a material's alert or change its reorder point directly.
* Notification list: search, filter, sort and pagination.
* Supplier saving moved to admin-post, so it no longer depends on output buffering.
* Fix: activating the plugin on a site that already had data stamped the database version to the newest value without running the version-specific migrations, so any migration step that dbDelta cannot express was skipped for good. Activation now goes through the same upgrade path as a normal update. In practice this meant the supplier table kept its old unique key, so a supplier could not be registered twice under the same name with different office names — which is exactly what the corporate-number release added the office name for.
* Readme fix: the feature list said the plugin has "three role tiers: director, staff lead, and staff". No such roles exist. The three roles are administrator, staff and view-only, as the plugin's own screens have always said. There is no "staff lead" role, and the "view-only" role was missing from the list.
* The product category grouping on the stock list now matches the companion web service. The "medicine" group is relabelled from "歯科用医薬品" to "医薬品", 68 keywords the web service had learned since this classifier was written are applied (bone graft materials, radiographic accessories, laboratory casting supplies, hand instruments and others), and a material whose generic name is empty is now grouped by its product name instead of being left unclassified. The same material could previously appear under one group here and another one on the web.

= 1.21.6 =

* Camera scanning no longer uses WebAssembly. The decoder is now the bundled pure-JavaScript @zxing/library, so the package contains no binary, has no optional component to install, and references no external host. It reads JAN/EAN, GS1-128, GS1 DataBar, GS1 DataBar Expanded, DataMatrix and QR. GS1 DataBar Limited is no longer decoded by the camera and is typed in by hand.
* Security fix: the staff PWA's offline snapshot endpoint returned stock summaries for every active branch to any signed-in PWA user. It is now authorised per branch, like every other screen, so staff and view-only accounts see only their own branch.
* Security fix: the staff PWA's stock use/dispose handler authorised the submitted branch but then loaded the lot by its id alone, so a staff account could decrement or dispose a lot belonging to a branch it has no access to. The lot is now always looked up bound to the already-authorised branch and material.
* Security fix: when registering a material from the staff PWA, the submitted image attachment id was trusted as-is. It is now accepted only if it is a pending upload made by the same operator.
* The PWA's stylesheet and page scripts are now served directly from the plugin directory with wp_enqueue_style/wp_enqueue_script instead of being streamed through a PHP route. The Service Worker is still returned by PHP, because it needs the Service-Worker-Allowed header to take /dzm-pwa/ as its scope.
* Login to the staff PWA is now CSRF-protected with a nonce, and all display parameters on PWA screens are read through registered query vars or a nonce-checked helper rather than from $_GET inside the templates.
* Input from $_POST is sanitized where it is read (email with sanitize_email, and so on) instead of later on save.
* The plugin no longer calls base64_encode() or base64_decode() anywhere. The encrypted API token is stored as hexadecimal, and the connection key's base64url payload is decoded with PHP's own sodium_base642bin().
* New: the settings screen now has a "revoke" button that disables the external service integration. It deletes the stored consent and access tokens, after which the plugin issues no API request at all — including the scheduled tasks. The plugin's own screens and readme said this was possible, but no such control existed. Local inventory data is kept.
* Fixed: redeeming an application code contacted the web service before consent had been given. It is now gated on consent like every other request, so the plugin really does send nothing until the administrator agrees.
* Fixed: on the "needs reordering" list, the link to the order screen passed the suggested quantity under an unregistered query name, so the order form always opened with a quantity of 1 instead of the shortfall.
* UPC-E was listed as a supported camera format, but the bundled decoder cannot read it, and a UPC-E code that did decode would have been handed to the JAN field in its compressed 8-digit form, matching nothing. It has been removed from the enabled formats and from the documentation. UPC-A, EAN-13 and EAN-8 are unaffected.
* Documentation: corrected several statements in readme.txt that no longer matched the code — the weekly recommendation pass runs three detectors, not four; there is no scheduled manufacturer-list refresh; the campaign digest runs on page view, not on a schedule; offline changes are not queued for later sync; and the lists of what is sent to the web service now name every field, including the material feedback body and the clinic's contact details.

= 1.21.5 =
* Security: all admin-screen GET parameters (display filters, edit links and post-redirect status flags) are now nonce-verified through a single helper; without a valid nonce the screen renders its default state. In the staff PWA, which authenticates with its own JWT session rather than WordPress cookies, the post-redirect banner parameters and the scan hand-off parameters are verified with an equivalent CSRF check before they are read.
* Security: fixed a branch authorization gap on the staff app home screen. The branch given in the query string was used without checking the operator's permission, so a staff or viewer account could read another branch's stock summary by changing it. The branch is now re-authorized with the same check used by the other screens, and the branch switcher is shown only to operators who are allowed to cross branches.
* The one-time dzm_* to dzmaker_* option migration now renames only this plugin's own options from a fixed list of known names instead of scanning wp_options with a LIKE 'dzm_%' pattern, so options belonging to other plugins can never be read, renamed or deleted by it.
* Removed a require of wp-admin/includes/media.php whose functions were never used (wp_handle_sideload and wp_generate_attachment_metadata come from file.php and image.php, which are still loaded and used immediately).
* Uninstall additionally removes two API response cache options.

= 1.21.4 =
* Database: every query now passes its table name to `$wpdb->prepare()` through the `%i` identifier placeholder instead of interpolating it into the SQL string. This covers all 117 call sites across 32 files, including `SHOW COLUMNS`, `ALTER TABLE` and `DROP TABLE`. Because `%i` requires WordPress 6.2, "Requires at least" is raised from 6.0 to 6.2.
* Code quality: removed all 286 file-level `phpcs:disable` comments. The plugin now reports zero errors and zero warnings under PHPCS with the WordPress Coding Standards sniffs that guard database access, request handling, escaping, i18n, prefixing and PHP/WP compatibility (`WordPress.DB`, `WordPress.Security`, `WordPress.WP.*`, `WordPress.NamingConventions.PrefixAllGlobals`, `PHPCompatibilityWP`); the remaining suppressions are all line-scoped and each carries a written justification. The `phpcs.xml.dist` and `composer.json` that reproduce this run are in the plugin's source repository (they are development tooling and are not part of the distributed package).
* Fixed: four call sites queried `wp_dzmaker_corporations` / `wp_dzmaker_suppliers`, tables that do not exist — the real tables are `wp_dzm_*`. As a result the supplier master and seller-linking features always read corporation ID 0, attaching a linked seller from the staff app always failed, and the 1.2.0 database migration silently skipped its supplier seeding and back-fill step.
* Fixed: `public/js/vendor/qrcode-generator.js` had one line altered from the upstream source (the x and y arguments of `renderTo2dContext()` were swapped). Restored to the unmodified upstream 2.0.4 file.
* Fixed: the overstock recommendation summed a column that does not exist in the inventory log table, so it errored on every weekly run and never produced a recommendation. It now works.
* The volume-discount and unused-event recommendations are disabled. They queried supplier columns that were never part of the database schema and nothing ever wrote them, so they have never produced a result; they now no longer log a database error on the weekly cron. Nothing that previously worked has been removed.
* Bundled libraries are now pinned in `package.json`, copied verbatim from upstream, and verifiable against `public/licenses/VENDOR-CHECKSUMS.txt` with `shasum -a 256 -c` from the plugin's root directory.
* Fixed: uninstalling the plugin left two of its twenty database tables behind (`..._dzm_linked_sellers` and `..._dzm_material_feedback`); they were never listed in the uninstaller. All twenty are now removed.
* Admin and PWA view templates now prefix their local variables with `dzmaker_`, which also resolves 28 collisions with WordPress core global variable names.

= 1.21.3 =
* Security: branch-level authorization is now enforced for the My Page inventory view and the PWA stock/order actions. Staff and view-only accounts are restricted to their own assigned branch; only administrators can switch branches. Previously the branch could be changed via a request parameter without a branch-level permission check.
* Privacy/uninstall: the uninstaller now deletes only this plugin's own known options by explicit name (both the current `dzmaker_` and legacy `dzm_` prefixes). It no longer runs a broad `LIKE 'dzm_%'` sweep, so options belonging to other plugins are never touched.
* Hardening: added explicit annotations to the remaining direct database reads (table-name-only queries with no user input; all user-supplied values already go through prepared statements).

= 1.21.2 =
* Compliance: renamed the plugin's internal code prefix from the 3-character "dzm" / "DZM_" to the 4+ character "dzmaker" / "DZMAKER_" across all constants, classes, options, transients, cron hooks, the my-page shortcode, and JavaScript globals, per the WordPress.org unique-prefix requirement. Existing installations are migrated automatically on upgrade (options, scheduled tasks, and the my-page shortcode are carried over; database tables are unchanged, so no data is lost).
* Fix: updated the Author URI to a fast, reliable address.

= 1.21.1 =
* Compliance: moved all inline `<script>` / `<style>` blocks in admin and front-end views into enqueued asset files (wp_enqueue_script / wp_enqueue_style / wp_add_inline_style / wp_localize_script). No functional change.
* Updated the bundled Chart.js library to 4.5.1.

= 1.21.0 =
* New: post requests / suggestions about a registered material to its manufacturer. The 使用材料一覧 (used-materials list) gains a 「投稿」 button per material; the form (with a clear purpose/policy notice, an anonymous toggle, and an "urgent defect" checkbox with an important-notice alert) shows a confirmation screen with 戻る/送信 before sending.
* New: 投稿済一覧 (posted list) screen with progress badges (緊急 / 通知済 / メーカー未開封 / 開封済). "メーカー未開封" means the manufacturer is either not registered in the system or has not opened the post yet.
* The post is stored on the Web side (system of record). Paid manufacturers can view posts limited to their own products on their Web mypage, and receive an email notification linking to the post. Urgent defects additionally notify the system administrator and (when configured) the dental associations.
* Hourly sync pulls the manufacturer "opened" / notification status back into the posted list. DB schema 1.7.0 → 1.8.0 (adds material_feedback).
* Used-materials list: the per-row 操作 / 投稿 buttons are now laid out side by side without arrows (投稿 is navy). Pressing 操作 now smooth-scrolls to a clearly highlighted operation panel (with a "back to list" link and a short description of what you can do there), and a one-line hint above the table explains both buttons.
* New: a Top (機能一覧) landing page placed at the very top of the admin menu. It presents every feature as a styled, categorized card (title + description) that links straight to the feature page. The former home (initial-setup steps + staff-app QR) is renamed to 「はじめに」 and kept directly below Top.

= 1.20.2 =
* Data import (CSV): the JAN code column now accepts GS1 codes too — a GS1 element string (e.g. "(01)…(17)…(10)…" or raw GS1-128/DataMatrix) is parsed to extract the GTIN (matched as a JAN) and, when the expiry / lot columns are blank, the expiry date (AI 17) and lot number (AI 10) are auto-filled.
* Import template: removed the "仕入先URL" column from the sample to keep it simple (the column is still accepted if present).

= 1.20.1 =
* Data import (CSV) screen: the "open the Web version" link now goes directly to the Web matching page (/clinic/import); after logging in there, the clinic lands straight on the import page.

= 1.20.0 =
* New: bulk CSV import for onboarding ("データ取り込み(CSV)" screen). Import past inventory and purchase history at once instead of entering items one by one. Required columns are just product name and quantity; a downloadable template is provided; UTF-8 and Shift_JIS are both accepted; items without a JAN code are supported (matched by in-store code or product name, or created as custom). A preview shows per-row status before committing.
* Two import modes (auto-detected per row): opening balance (current on-hand stock, not counted as an expense) and purchase history (past purchases with date/price, fed into the expense dashboard and price trend, without inflating current stock — no double counting).
* Guidance to optionally match official product names on the Web side first, then import the cleaned CSV. Added an optional onboarding step.

= 1.19.10 =
* i18n maintenance: regenerated the translation template (languages/dentalzaiko-maker.pot) from the current source with wp-cli, and re-synced the bundled Japanese catalog (ja.po/ja.mo). No functional change.

= 1.19.9 =
* Code quality: resolved all WordPress Plugin Check findings in the seller-orders feature files — escaped integer-ID output in the staff/supplier/order PWA views, added a missing "translators:" comment, and annotated known-safe SQL (table-name interpolation, dynamic IN() placeholders) and the dzm_ variable prefix as phpcs-ignored, consistent with the rest of the plugin. No functional or behavioral change.

= 1.19.8 =
* Home screen: the staff app login URL is now shown in a prominent highlighted "重要" box at the bottom, with a QR code so staff can open the app by scanning with their phone, plus a one-tap "copy URL" button. The QR code is generated locally in the browser (bundled MIT qrcode-generator library) — no external request is made.

= 1.19.7 =
* Public-institution report (manual send): when the previous month has no aggregated data (e.g. right after introduction), the screen now shows a clear amber warning explaining nothing was sent and that it is not an error, instead of the cryptic "result: empty" message. Successful sends now also show the target month and item count.

= 1.19.6 =
* Notifications screen: the delivery-channel labels are now shorter — 両方 / メール / 画面 (was 画面＋メール / メールのみ / 画面のみ).
* Notifications screen: the severity badges now use crisper traffic-light colors (緊急 = vivid red / 注意 = vivid amber / 情報 = vivid green) for better at-a-glance visibility.

= 1.19.5 =
* Notifications screen: the severity is now shown as a bold colored badge (緊急 = red / 注意 = amber / 情報 = green) so it stands out at a glance, and the delivery channel is shown in plain Japanese (画面＋メール / メールのみ / 画面のみ) instead of the raw value "both" etc.

= 1.19.4 =
* Email signature (common footer) refreshed: the system name is now a single line ("歯科在庫管理システム DentalZaikoMaker") and the facility contact details (担当 / 住所 / TEL / Email) are indented under the facility name for a cleaner signature.
* Notification emails (expiry warning, low-stock, seller-order status) now open with an addressee line — the recipient's name when it can be resolved (staff / WordPress user), otherwise a generic "在庫管理ご担当者 様".
* Low-stock email now includes a body (material name / remaining quantity / threshold) instead of subject-only; expiry email body now also states the material name; seller-order status email adds a "check your order history" guidance line; internal order-request notification now addresses each administrator.

= 1.19.3 =
* Public-institution report screen: the participation request is now shown as a large warning banner at the very top of the page, and it is hidden once the clinic has opted in.

= 1.19.2 =
* Fix: the cost-saving recommendations screen was always empty — its SQL selected a non-existent `branch_name` column from the branches table (column is `name`), so the query silently failed. Also fixed the same wrong-column bug in the accounting CSV exports (freee / MoneyForward / transaction list), which were producing empty files.
* The recommendations screen now shows the "detection batch" result message after running it, and avoids a wpdb::prepare placeholder notice on the unfiltered ("all") view.

= 1.19.1 =
* Public-institution report screen renamed to "公的機関レポート(価格状況の月次通知)".
* Added an optional onboarding step ("月次価格レポートに参加する(任意)") and an encouragement note on the report screen to make participation easy to discover. The monthly report remains strictly opt-in (default OFF) per WordPress.org guideline 7; the step completes once the admin makes a choice (join or decline), so it never auto-enables data sending.

= 1.19.0 =
* Inventory list usability: both the admin "Inventory" screen and the staff My Page stock list now have a search box (matches material name, JAN, in-store code, or maker) and a sort dropdown (earliest expiry first, lowest stock, reorder-needed first, name, most recently received). The admin screen filters/sorts server-side via the existing branch form; the My Page list does it client-side (no reload), combined with the existing category chips. No database changes.
* Admin menu: "材料" submenu renamed to "材料一覧". Seller-link screen now explains that seller codes are issued from the partner portal (healthbanks.org/dzm).
* Fix: the expense dashboard's "monthly trend" chart was always empty because its SQL aliased a reserved word (`year_month`) without quoting, so the query silently failed on MySQL/MariaDB. Also fixed the branch labels on the expense dashboard and branch-comparison charts (were reading a non-existent `branch_name` column instead of `name`).

= 1.18.0 =
* Notification emails now carry a consistent signature footer — the facility name (clinic or laboratory) followed by its contact details (representative, address, phone, email; only the fields that are filled in) — and a short purpose line at the top tailored to each notification type. Plain-text only; login links and secrets are never embedded.
* Web seller orders: when a linked seller updates an order's status (order confirmed / shipped / declined), the staff member who requested the order is now notified by email in addition to the in-app notice.
* Admin "Notifications" screen: the body column is now left-aligned (fixed a white-space:pre-wrap indentation glitch that pushed the text to the right), and the "type" column shows readable Japanese labels (e.g. "使用期限", "在庫しきい値", "受注確認(販売店)") instead of raw codes — the raw code is still available on hover.

= 1.17.0 =
* Sellers (販売店), Phase 5: order status now syncs back automatically. An hourly cron polls each linked seller for status changes (order confirmed / shipped / declined), updates the clinic's order history, and notifies the staff member who requested the order (in-app).

= 1.16.0 =
* Sellers (販売店), Phase 4: clinics can now send approved orders to a linked seller through the web order API instead of email. The order-management screen gains a "send web order" action for sellers connected by code. Database upgrade to 1.7.0 (seller order tracking columns: web order no. and status). Applied automatically on update; no data loss.

= 1.15.0 =
* Sellers (販売店), Phase 3: a clinic can link a dealer by seller code. A new "seller linkage" admin screen calls the web API to connect; once linked, the seller appears as a selectable supplier on the order screens. Database upgrade to 1.6.0 (local seller-linkage table). Applied automatically on update; no data loss.

= 1.14.0 =
* Ordering: a web-recommended company that is not yet registered as a supplier can now be added as a supplier in one click directly from the order screen, and the action-select screen shows a supplier card. This makes it quicker to turn a recommendation into an actual order.

= 1.13.0 =
* Inventory list (staff mypage): added a product major-category filter. The in-house stock list now shows a category chip bar (orthodontics, implant, crown/prosthetic, cutting tools, etc.) and a small category badge on each item, so staff can quickly narrow a long inventory by category. Filtering is client-side and works offline.
* Medicines are sub-labelled by the JDTA dental medicine classification (local anesthetic, antibiotic, caries-prevention, root-canal, etc.) under the "dental medicine" category.
* Category is derived locally from the cached generic name, so it works even before the next server sync.

= 1.12.0 =
* Product major-category (大分類): materials now carry an 18-way product category (orthodontics, implant, crown/prosthetic, etc.) derived on the web side from the official generic name. The plugin caches it per material (new local_materials.major_category column) on registration and during sync, enabling category-grouped browsing/filtering of in-house inventory.
* Material sync now also refreshes the cached major-category from the server.
* Database upgrade to 1.5.0 (adds local_materials.major_category). Applied automatically on update; no data loss.

= 1.11.0 =
* Barcode scanner (staff PWA): packaging GTIN-14 support. The scanner now passes the raw GS1 GTIN-14 (e.g. a sales-pack barcode with package indicator 1) instead of collapsing it to a 13-digit JAN, so packaging-level codes that have no matching JAN now resolve correctly.
* Material matching: local lookup is now GTIN-first. The plugin remembers the actual scanned GTIN per material (new "learned GTIN" column), so re-scanning the same box hits locally without a server round-trip. Falls back to JAN normalization (indicator 0 → 13-digit JAN) and then the Web master lookup (now queried by gtin).
* Database upgrade to 1.4.0 (adds local_materials.gtin). Applied automatically on update; no data loss.

= 1.10.0 =
* Barcode scanner (staff PWA): GS1/UDI support. Scanning a medical-supply GS1 code (1D GS1-128 / GS1 DataBar, or 2D DataMatrix/QR) now auto-fills the lot number (AI 10) and expiry date (AI 17) on the stock-in form — no more manual typing. JAN-only products are unaffected (lot/expiry remain manual as before).
* Scanner now also reads 1D GS1 barcodes (GS1-128 / GS1 DataBar Expanded), in addition to the previously supported JAN/EAN and 2D GS1 DataMatrix/QR.
* Expiry AI(17) YYMMDD is converted to a real date (DD=00 → end of month); lot AI(10) is read up to 32 characters. Auto-filled values are shown with a "scanned" badge and remain editable.

= 1.9.0 =
* Events (特売): switched the staff mypage event matching from per-JAN API calls to a single "active events digest" fetched with conditional GET (ETag/304) and matched locally against in-house inventory. This keeps central server load independent of the number of clinics and inventory size. Order-time details still use the per-JAN recommendations API.
* Mypage: the 販促イベント (sale) list now also shows the discount rate fallback and the partner company name in the alert.
* Requires the Web side (healthbanks.org/dzm) to expose /plugin/events/active-digest; the plugin falls back gracefully (cached/empty) when unavailable.

= 1.8.4 =
* Terminology: unified the supplier label to "仕入先" across the whole plugin (staff PWA, admin material/inventory/order screens, and shared messages), replacing the older mixed "購入先" wording. Display text only — no change to data, validation, or order processing.

= 1.8.3 =
* Staff My Page: fixed the "Staff info" panel not appearing on iPhone/iOS Safari — tapping the tile now reliably opens the panel and scrolls it into view (the previous CSS-only reveal did not scroll to the panel on iOS).

= 1.8.2 =
* Staff My Page: tapping the "Staff info" tile now opens an info panel showing the signed-in staff's name, role, branch, login method, and email, with a logout button (previously it only scrolled to the top with no feedback).

= 1.8.1 =
* Staff app: the stock list is now its own full screen, and a new admin-only staff registration screen was added (register/edit staff with role, branch, and PIN; reuses the existing invite-email/PIN flow). Staff information can only be changed by administrators (with an on-screen note).
* Staff app: added a consistent quick-navigation header (Home / Stock-in / Stock list) to the supplier-registration, stock-in (scan), stock-list, and staff-registration screens.

= 1.8.0 =
* Suppliers can now store an email address and a contact-person name (staff PWA registration form and the admin supplier editor).
* The staff PWA supplier screen now lists the registered suppliers below the registration form, each expandable to edit its details (name, kana, URL, email, contact person, notes, active) inline.
* Order emails: from the admin "Order requests" screen, approved orders that have not yet been emailed are grouped per supplier (for suppliers that have a registered email) and can be sent as a single combined order email per supplier. The email lists each product and quantity and is sent with the clinic's contact info as the reply-to. Suppliers without an email continue to use the existing EC redirect link.
* DB schema 1.3.0: adds suppliers.email / suppliers.contact_person and order_history.order_email_sent_at / order_email_to / order_email_batch_id (auto-migrated on update).

= 1.7.1 =
* Staff My Page: the "Log in with email and PIN" control on the login screen is now a button matching the WordPress login button above it, so it is easier to recognize as tappable.
* Staff My Page: the summary chips (reorder / expiring soon / on-sale) now open a dedicated full-screen filtered list for that category (with a "back to My Page" link) instead of a static badge, including a new "Expiring soon (use-by)" list.
* Staff PWA: the "back to action select" link on the stock-in/out and order screens is now a button, consistent with the "scan another code" button.
* Staff PWA: when a material has no stock (lot) at the branch, the use/dispose screen no longer shows an unusable empty lot selector; it now shows a clear "no stock" message with a shortcut to record a stock-in. When only one lot exists it is pre-selected.

= 1.7.0 =
* Staff PWA scanner now reads 2D codes (GS1 DataMatrix and QR) in addition to JAN/EAN barcodes, so medical-supply packaging that carries a GS1 DataMatrix can be scanned. GTIN-14 values are converted to the 13-digit JAN to match existing inventory.
* Replaced the barcode engine for reliable DataMatrix/QR recognition on iOS Safari, where the native BarcodeDetector API is unavailable. (Superseded in 1.21.6, which decodes with a bundled pure-JavaScript library.)
* Scanner UX: the on-screen frame now matches the actual scan region, the guidance asks the user to hold the camera slightly farther for focus, and an optional flashlight (torch) button appears on devices that support it.
* Fixed an issue where re-opening the scanner from "scan again" could show the previously scanned value (iOS Safari back-forward cache); the scanner now resets on each view. Added a "scan another code" link to the material detail screen.
* Product images are now downscaled to fit within 240x240 pixels and re-encoded on upload, reducing storage on the server. Material images in the stock lists (staff page and admin) can be tapped to view an enlarged version with a close button.

= 1.6.0 =
* Added a login-gated, tablet/phone-optimized staff inventory page via the `[dzm_mypage]` shortcode (a "My Page" hub): camera JAN scanning, product images, an enriched stock list with reorder and sale-event alerts, supplier registration, and quick ordering.
* Dual authentication for the staff page and operations: WordPress login (mapped to the staff role and branch) or the existing email + PIN session. Viewer-only accounts cannot perform operations.
* Administrators can place orders from the staff page without a separate approval step (auto-approve); staff orders still go through clinic-director approval.
* Admin: made the "apply / get a connection key" link a prominent button on the connection settings screen, and added an input helper on the Add Location screen to copy the main clinic's registered details.

= 1.5.0 =
* Improved the connection-key onboarding flow: a single primary path for the connection key, a connection-status card (unconfigured / saved / connected / error) with masked token display, inline paste-mistake detection, separation of key reveal vs. reissue, and reference numbers on authentication errors.

= 1.4.0 =
* Added an OPTIONAL monthly statistics report (opt-in, OFF by default) that sends aggregated per material/category monthly price statistics for the previous month to the companion web service, to help public bodies (dental associations, the Ministry of Health, Labour and Welfare) understand material price trends. Raw orders, patient, and clinical data are never sent.
* New "Public report" settings screen: explicit opt-in toggle, sender anonymization toggle (anonymous by default), a disclosure of exactly what is sent, and a required acknowledgement. Sending can be turned off at any time.
* The report runs on a monthly WP-Cron schedule only after opt-in; when not opted in, no data is sent.
* Added a "Price trend (own clinic)" admin screen that visualizes the clinic's own monthly average unit price by category (Chart.js). This reads local data only and never sends anything externally.
* Updated the readme disclosure and privacy policy to describe the optional outbound monthly report.

= 1.3.0 =
* Added a corporation-wide supplier master so administrators can pre-register suppliers from a dedicated admin page; staff can then pick from the list on the PWA material registration screen instead of typing supplier names every time.
* Inline-create flow on the PWA: choosing "+ 新しい仕入先を追加" lets staff create a master record on the fly without leaving the registration form.
* Added an event alert on the PWA material registration screen: when the master DB returns one or more active events for the same product, a deep-navy info card surfaces the offers and links to the web service event detail page.
* DB migration: new wp_dzm_suppliers table (corporation-scoped, unique by name) plus a supplier_id foreign key on wp_dzm_material_suppliers. Existing material_suppliers.company_name_cache values are auto-aggregated into the master and back-filled on upgrade.

= 1.2.1 =
* Added a global navigation drawer (hamburger menu) to the staff PWA so users can jump between Home, Scan, New material, Inventory list, and Logout from any sub-page.
* Improved Lighthouse Accessibility score to 100 on PWA pages: associated `<select>` elements with `<label for>`, fixed heading order (`<h3>` → `<h2>`), added `role="img"` + `aria-label` to all Chart.js canvases, and grouped radio sets with `<fieldset><legend>`.
* Material edit preview image now has a descriptive alt attribute instead of an empty one.

= 1.2.0 =
* UX/UI overhaul: deep-navy primary (#1e3a8a, AAA 8.59:1) + red CTA accent applied to all 26 admin and PWA views, with Nielsen 10 heuristics and consistent BEM `.dzm-` prefix.
* Hardened the opt-in consent flow: pre-consent error notice in red, blinking call-to-action, and stronger guard rails (`dzm_consent_required` is checked at JWT acquisition and at every API call).
* Image upload now compresses to display-required size (long edge 1600px, JPEG 0.85) on the client side before posting, both in the admin material editor and the staff PWA.
* WP.org Plugin Check fully clean (0 errors / 0 warnings) — the plugin is now ready for the WordPress.org submission.

= 1.1.0 =
* Added product image upload from the staff PWA (camera/library) and image display across PWA and admin screens.
* New PWA flow: register a brand new material directly from a smartphone, including a product photo, in a single screen.
* DB migration: added local_materials.image_attachment_id (links to the WordPress Media Library).

= 1.0.1 =
* Added detection of JDTA master changes (discontinuation warnings and name change notifications).
* Order forms can now choose between the latest master name and the locally saved name; historical records keep the original name.
* Added a "Refresh from server" button on the material detail screen, shown only when the master has changed.
* DB migration: added name snapshot columns to local_materials, order_requests, and order_history.

= 1.0.0 =
* Initial release.
