Merge commit '36d5feed172cb12ccd469abddebf04eaf9a7c9ae' as 'cgilite'
authorPaul Hänsch <paul@plutz.net>
Sat, 2 Jan 2021 09:20:13 +0000 (10:20 +0100)
committerPaul Hänsch <paul@plutz.net>
Sat, 2 Jan 2021 09:20:13 +0000 (10:20 +0100)
67 files changed:
.gitmodules [new file with mode: 0644]
COPYING [new file with mode: 0644]
Makefile [new file with mode: 0644]
cards/cards.css [new file with mode: 0644]
cards/categories/categories.css [new file with mode: 0644]
cards/categories/categories.html.sh [new file with mode: 0755]
cards/categories/categories.sh [new file with mode: 0755]
cards/categories/edit_categories.sh [new file with mode: 0755]
cards/categories/text_categories.sh [new file with mode: 0755]
cards/categories/update_categories.sh [new file with mode: 0755]
cards/edit_card.sh [new file with mode: 0755]
cards/export_card.sh [new file with mode: 0755]
cards/filter_card.sh [new file with mode: 0755]
cards/index.cgi [new file with mode: 0755]
cards/l10n.sh [new file with mode: 0755]
cards/list.sh [new file with mode: 0755]
cards/new_card.sh [new file with mode: 0755]
cards/update_card.sh [new file with mode: 0755]
cards/widgets.sh [new file with mode: 0755]
cgilite/cgilite.sh [moved from cgilite.sh with 100% similarity]
cgilite/file.sh [moved from file.sh with 100% similarity]
cgilite/html-sh.sed [moved from html-sh.sed with 100% similarity]
cgilite/logging.sh [moved from logging.sh with 100% similarity]
cgilite/session.sh [moved from session.sh with 100% similarity]
cgilite/storage.sh [moved from storage.sh with 100% similarity]
courses/course_print.sh [new file with mode: 0755]
courses/courses.css [new file with mode: 0644]
courses/courses.sh [new file with mode: 0755]
courses/edit_course.sh [new file with mode: 0755]
courses/edit_course.template.sh [new file with mode: 0755]
courses/export_ical.sh [new file with mode: 0755]
courses/generate_courselist.sh [new file with mode: 0755]
courses/index.cgi [new file with mode: 0755]
courses/l10n.sh [new file with mode: 0755]
courses/list.sh [new file with mode: 0755]
courses/new_course.sh [new file with mode: 0755]
courses/update_course.sh [new file with mode: 0755]
courses/view_course.sh [new file with mode: 0755]
courses/widgets.sh [new file with mode: 0755]
email/email.css [new file with mode: 0644]
email/email.html.sh [new file with mode: 0755]
email/email.sh [new file with mode: 0755]
frame.html.sh [new file with mode: 0755]
globals.sh [new file with mode: 0755]
index.cgi [new file with mode: 0755]
pdiread.sh [new file with mode: 0755]
prescriptions/edit_prescription.sh [new file with mode: 0755]
prescriptions/new_prescription.sh [new file with mode: 0755]
prescriptions/prescriptions.css [new file with mode: 0644]
prescriptions/prescriptions.html.sh [new file with mode: 0755]
prescriptions/prescriptions.sh [new file with mode: 0755]
prescriptions/text_prescriptions.sh [new file with mode: 0755]
prescriptions/update_prescription.sh [new file with mode: 0755]
prescriptions/view_prescription.sh [new file with mode: 0755]
session_lock.sh [new file with mode: 0644]
shcgi [new submodule]
style.css [new file with mode: 0644]
text_frame.sh [new file with mode: 0755]
therapy/text_therapy.sh [new file with mode: 0755]
therapy/therapy.css [new file with mode: 0644]
therapy/therapy.html.sh [new file with mode: 0755]
therapy/therapy.sh [new file with mode: 0755]
therapy/therapy_background.png [new file with mode: 0644]
therapy/therapy_background.xcf [new file with mode: 0644]
therapy/therapy_draw.js [new file with mode: 0644]
therapy/update_therapy.sh [new file with mode: 0755]
update_bookmarks.sh [new file with mode: 0755]

diff --git a/.gitmodules b/.gitmodules
new file mode 100644 (file)
index 0000000..fb39709
--- /dev/null
@@ -0,0 +1,3 @@
+[submodule "shcgi"]
+       path = shcgi
+       url = http://git.plutz.net/git/shcgi
diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..dba13ed
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,661 @@
+                    GNU AFFERO GENERAL PUBLIC LICENSE
+                       Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+  A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate.  Many developers of free software are heartened and
+encouraged by the resulting cooperation.  However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+  The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community.  It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server.  Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+  An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals.  This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU Affero General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Remote Network Interaction; Use with the GNU General Public License.
+
+  Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software.  This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time.  Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source.  For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code.  There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<http://www.gnu.org/licenses/>.
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..24781a9
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,9 @@
+.PHONY: _subtrees
+
+_subtrees: _cgilite
+
+cgilite:
+       git subtree add --squash -P $@ https://git.plutz.net/git/$@ master
+
+_cgilite: cgilite
+       git subtree pull --squash -P $< https://git.plutz.net/git/$< master
diff --git a/cards/cards.css b/cards/cards.css
new file mode 100644 (file)
index 0000000..6349f8a
--- /dev/null
@@ -0,0 +1,349 @@
+/*
+# Copyright 2014 - 2017 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+*/
+
+.filter, .newcard, .card { box-shadow: 0 0 1ex; }
+
+.filter {
+  display: block;
+  border: solid 1px;
+  border-radius: 4px 4px 0 0;
+  margin: .5em 2em .25em 2em;
+  padding: 0 2ex .5em 2ex;
+  background: #EFF;
+}
+
+.filter h1 {
+  display: block;
+  font-weight: bold;
+  font-size: 1.25em;
+  border-style: none none solid none;
+  border-radius: 4px 4px 0 0;
+  border-width: 1px;
+  margin: 0 -1.625ex .5em -1.625ex;
+  padding: .125em 1ex;
+  background: #EEF;
+}
+.filter input[type="text"] {
+  width: 100%;
+  height: 2em;
+  margin-bottom: 1em;
+}
+.filter fieldset {
+  position: relative;
+  display: block;
+  border: none;
+  padding: 0 0 0 12ex;
+  margin: 0 0 .5em 0;
+}
+.filter fieldset legend {
+  position: absolute;
+  font-size: 1em;
+  font-weight: bold;
+  margin-left: -12ex;
+}
+.filter label {
+  display: inline-block;
+  vertical-align: top;
+  margin: .125em 2ex 0 0;
+  white-space: nowrap;
+}
+.filter button {
+  margin-top: .5em;
+}
+
+.filter fieldset.item { padding-left: 0; }
+.filter fieldset.item legend { display: none; }
+.filter fieldset.item input[type=text],
+.filter fieldset.item fieldset.categories {
+  background-color: #FFF;
+  border: 1px solid #000;
+  margin-top: -1px;
+  padding: .25em .5em;
+}
+.filter fieldset.item input[type=radio] { display: none; }
+.filter fieldset.item input[type=radio] + label {
+  position: relative; z-index: 1;
+  margin: 0; padding: .25em 1em;
+  
+}
+.filter fieldset.item input[type=radio]:checked + label {
+  background-color: #FFF;
+  border: 1px solid #000;
+  border-bottom-color: #FFF;
+}
+.filter fieldset.item input[type=radio][value="CATEGORIES"]:checked + label + input[type=text],
+.filter fieldset.item input[type=radio][value="CATEGORIES"] + label + input[type=text] + fieldset.categories {
+  display: none;
+}
+.filter fieldset.item input[type=radio][value="CATEGORIES"]:checked + label + input[type=text] + fieldset.categories {
+  display: block;
+}
+.filter fieldset.item fieldset.categories a {
+  font-size: .875em;
+  line-height: 1.625em;
+  color: #44C;
+}
+
+.newcard {
+  display: block;
+  border: solid 1px;
+  margin: 1em 2em .5em 2em;
+  padding: .5em;
+  text-align: right;
+  background: #EEF;
+}
+.newcard button {
+  box-shadow: 0 0 .5ex #888;
+}
+
+.card {
+  display: block;
+  position: relative;
+  border: solid 1px;
+  margin: .25em 2em;
+  padding: 0 20ex 0 0;
+  overflow: auto;
+  background: #FFF;
+  min-height: 8em;
+}
+.card .control {
+  display: inline-block;
+  position: absolute;
+  top: 0; right: 0;
+  text-align: right;
+  background: #EEF;
+  margin: 0;
+  height: 100%;
+  width: 20ex;
+  max-width: 33%;
+  padding: .25em .5em;
+}
+.card .control .item {
+  display: inline-block;
+  width: 100%;
+  height: 2.5em;
+  line-height: 2.5em;
+  margin: .25em 0;
+  padding: 0 .5em;
+  font-size: .75em;
+  color: #008;
+  background: #FFF;
+  text-decoration: none;
+  border: 1px solid #888;
+  box-shadow: 0 0 .5ex #888;
+}
+.card .control button.item { text-align: right;}
+.card .control .item:hover{ border-color: #008; box-shadow: 0 0 .5ex #008; }
+
+.card .control .item[name=newfield] {
+  width: calc(100% - 2.5em);
+}
+.card .control .item[value=addfield] {
+  width: 2.5em;
+  border-style: solid solid solid none;
+  text-align: center;
+}
+
+.card .control #delete + label + button,
+.card .control #delete { display: none; }
+.card .control #delete + label { background-color: #FEE; }
+.card .control #delete:checked + label + button {
+  position: fixed; display: block;
+  left: 40%; top: 30%; width: 20%;
+  font-size: 1.5em;
+  text-align: center;
+  background-color: #FAA;
+  color: #333;
+  border-radius: .25em 0 0 .25em;
+}
+.card .control #delete:checked + label:after {
+  position: fixed; display: block;
+  left: 60%; top: 30%; width: 5%;
+  margin: .25em 0; padding: .5em .75ex 0 .75ex;
+  height: 1.5em;
+  font-size: 2em;
+  content: 'X';
+  text-align: center;
+  color: #AAA; background-color: #000;
+  border-radius: 0 .25em .25em 0;
+}
+
+.card .section {
+  display: inline-block;
+  vertical-align: top;
+  float: left;
+  width: 16.625%;
+  min-width: 10em;
+  min-height: 6em;
+  margin: .125em 0 .5ex 0;
+  padding: 0 1ex .25em 1ex;
+}
+.card .section:nth-of-type(2n){ background: #EEE;}
+
+.card .section h2, .card .section h3 {
+  font-size: 1em;
+  display: block;
+  font-weight: bold;
+  margin: .25em -.5ex .25em -.5ex;
+  border-style: none none solid none;
+  border-width: 1px;
+}
+.card .section .FN {  /* will override h2 */
+  font-size: 1.2em;
+  margin-top: 0;
+}
+
+.card .section .item {
+  display: block;
+  max-width: 100%;
+  word-wrap: break-word;
+  white-space: pre-line;
+}
+.card .section .item label {
+  font-weight: bold;
+}
+
+.card .section textarea.NOTE {
+  min-height: 6em;
+}
+.card .section textarea.ADR {
+  min-height: 4em;
+}
+
+.card .section .PHOTO {
+  width: 100%;
+  max-height: 10em;
+}
+.card .section .GENDER { display: inline-block; padding-right: .5ex; }
+.card .section .BDAY { display: inline-block;}
+.card .section .TEL .type { color: #444; }
+
+.card .section select {
+  display: block;
+  background-color: #FFF;
+  border: 1px solid black;
+}
+
+.card .section.attendance ul { margin: 0; padding: 0; }
+.card .section.attendance ul li {
+  display: block;
+  word-wrap: break-word;
+}
+
+.card .section.prescriptions ul { margin: 0; padding: 0; }
+.card .section.prescriptions ul li {
+  display: block;
+  word-wrap: break-word;
+  margin: 0 .25ex;
+}
+
+.card .section.prescriptions { width: 40ex;}
+
+form.card input.delete { display: none; }
+form.card input.delete + label {
+  display: inline-block;
+  position: relative;
+  font-size: .875em;
+  left: calc(100% - 1.5em);
+  width: 1.5em;
+  margin-bottom: -1.5em;
+  overflow: hidden;
+  white-space: nowrap;
+  vertical-align: bottom;
+}
+form.card input.delete + label:before {
+  display: inline-block;
+  content: 'x';
+  width: calc(1.5em - 2px); height: calc(1.5em - 2px);
+  vertical-align: bottom;
+  text-align: center;
+  color: #444;
+  background-color: #FBB;
+  border: 1px solid #BBB;
+}
+form.card input.delete + label + input[type=text],
+form.card input.delete + label + input:not([type]),
+form.card input.delete + label + select + input.TEL {
+  width: calc(100% - 1.5em);
+}
+form.card input.delete[name^=TEL_] + label { top: 1.5em; }
+form.card input.delete[name^=TEL_] + label:before { border-top: none; }
+form.card input.delete[name^=NOTE_] + label,
+form.card input.delete[name^=ADR_] + label { margin-bottom: 0; }
+form.card input.delete[name^=NOTE_] + label:before,
+form.card input.delete[name^=ADR_] + label:before { border-bottom: none; }
+
+form.card input.delete:checked + label,
+form.card input.delete:checked + label + input,
+form.card input.delete:checked + label + select,
+form.card input.delete:checked + label + select + input,
+form.card input.delete:checked + label + textarea {
+  display: none
+}
+
+form.card .section input[type=text],
+form.card .section input:not([type]),
+form.card .section textarea,
+form.card .section select {
+  width: 100%;
+  margin-bottom: .25em;
+}
+
+form.card .section select.teltype { margin-bottom: 0; }
+form.card .section input.TEL { margin-bottom: .5em; }
+
+form.card .attendance {
+  min-width: 66%;
+}
+form.card .attendance label {
+  position: relative;
+  display: inline-block;
+  width: 25ex;
+  padding-left: 3ex;
+}
+form.card .attendance label input[type=checkbox] {
+  position: absolute;
+  margin-left: -3ex;
+}
+
+form.card .insurance input[type=radio] {display: none;}
+form.card .insurance input[type=radio] + label {
+  display: inline-block;
+  width: 50%;
+  padding: .25ex 0;
+  text-align: center;
+  border: 1px solid black;
+  
+}
+form.card .insurance input[type=radio]:checked + label {
+  font-weight: bold;
+  background-color: #FFF;
+  border-width: 1px 1px 0 1px;
+}
+form.card .insurance input[type=radio] + label + input + label + select,
+form.card .insurance input[type=radio] + label + select + input {display: none;}
+form.card .insurance input[type=radio]:checked + label + input + label + select,
+form.card .insurance input[type=radio]:checked + label + select + input {
+  display: block;
+  border: 1px solid black;
+  background-color: #FFF;
+  border-width: 0 1px 1px 1px;
+  padding: .25ex 0;
+  margin-top: -1px;
+}
diff --git a/cards/categories/categories.css b/cards/categories/categories.css
new file mode 100644 (file)
index 0000000..0a7f41f
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+# Copyright 2014, 2015, 2018 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+*/
+
+form.categories {
+  display: block;
+  border: solid 1px;
+  border-radius: 4px 4px 0 0;
+  margin: .5em 2em .25em 2em;
+  padding: 0 2ex .5em 2ex;
+  background: #EFF;
+}
+
+form.categories h1 {
+  display: block;
+  font-weight: bold;
+  font-size: 1.25em;
+  border-style: none none solid none;
+  border-radius: 4px 4px 0 0;
+  border-width: 1px;
+  margin: 0 -1.625ex .5em -1.625ex;
+  padding: .125em 1ex;
+  background: #EEF;
+}
+
+form.categories ul { margin: 0; padding: 0; }
+form.categories ul li {
+  display: inline-block;
+  margin: 0 .25ex;
+  padding: .125em .25ex .125em 1ex;
+  border: 1px solid black;
+  background-color: #EEF;
+}
+form.categories ul li button {
+  margin: 0 0 0 1ex;
+  background-color: #FCC;
+  border: 1px solid black;
+  min-width: 3ex;
+  text-align: center;
+}
+form.categories ul li:last-of-type { padding: .125em .25ex .125em .25ex; }
+form.categories ul li:last-of-type button { background-color: #FFF; }
+
+form.namelist {
+  display: block;
+  margin: 1em 2em;
+  padding: 0;
+}
+form.namelist > fieldset {
+  display: block;
+  border: solid 1px;
+  border-radius: 4px 4px 0 0;
+  margin: 0; padding: .25em 2ex;
+  background: #EFF;
+  text-align: right;
+}
+form.namelist > fieldset:last-of-type {
+  border-radius: 0 0 4px 4px;
+}
+ul.namelist { margin: 0; padding: 0;}
+ul.namelist > li {
+  display: block;
+  border: solid 1px;
+  margin: -1px 0;
+  background: #FFF;
+}
+ul.namelist > li:nth-of-type(2n){ background: #EEE;}
+
+ul.namelist h2 {
+  font-size: 1em;
+  display: inline-block;
+  margin: 0;
+  padding: 0 1ex;
+  min-width: 30ex;
+}
+ul.namelist ul { margin: 0; padding: 0; display: inline-block;}
+ul.namelist ul li {
+  display: inline-block;
+  margin: 0 1ex 0 0;
+}
+
+.fakeorder {
+  position: absolute;
+  width: 0; height: 0;
+  left: -100%;
+}
diff --git a/cards/categories/categories.html.sh b/cards/categories/categories.html.sh
new file mode 100755 (executable)
index 0000000..75223bb
--- /dev/null
@@ -0,0 +1,74 @@
+# Copyright 2015, 2017, 2018 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+cat_listing(){
+  list_categories | while read cat; do
+    printf '<li>%s<button type="submit" name="remove" value="%s">%s</button></li>\n' \
+           "$(htmlsafe "$cat")" "$(attribsafe "$cat")" "$(l10n cat_remove)"
+  done
+}
+
+list_catsel(){
+  card="$1"
+  cats="$(get_categories "$card")"
+
+  list_categories |while read cat; do
+    printf '<li><label><input %s type="checkbox" name="%s" value="%s">%s</label></li>' \
+          "$(printf %s "$cats" |grep -qF "$cat" && printf 'checked="checked"')" \
+           "$(attribsafe "$card")" "$(attribsafe "$cat")" "$(htmlsafe "$cat")" 
+  done
+}
+
+display_catsel(){
+  card="$1"
+  printf '<li><h2>%s</h2><ul>' "$(htmlsafe "$(get_name "$card")")"
+  list_catsel "$card"
+  printf '</ul></li>'
+}
+
+cat <<EOF
+<form class="categories" action="?action=edit_categories" method="POST">
+  <input type="submit" class="fakeorder" name="add" value="add" />
+  <h1>$(l10n categories_label)</h1>
+  <input type="hidden" name="page" value="categories"/>
+  <ul>
+    $(cat_listing)
+    <li>
+      <input type="text" name="newcat" placeholder="$(l10n cat_newlabel)">
+      <button type="submit" name="add" value="add">$(l10n cat_add)</button>
+    </li>
+  </ul>
+</form>
+
+<form class="namelist" action="?action=update_categories" method="POST">
+  <fieldset>
+    <button type="submit" name="submit" value="submit">$(l10n cat_update)</button>
+  </fieldset>
+  <ul class="namelist">
+EOF
+listcards \
+| while read card; do
+  display_catsel "$card"
+done
+cat <<EOF
+  </ul>
+  <fieldset>
+    <button type="submit" name="submit" value="submit">$(l10n cat_update)</button>
+  </fieldset>
+</form>
+EOF
+
diff --git a/cards/categories/categories.sh b/cards/categories/categories.sh
new file mode 100755 (executable)
index 0000000..e875eef
--- /dev/null
@@ -0,0 +1,51 @@
+#!/bin/zsh
+
+# Copyright 2015, 2017 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+catfile="${_DATA}/mappings/categories"
+
+listcards() {
+  for file in "${_DATA}/vcard/"*.vcf; do
+    printf '%s\t%s\n' \
+      "$(sed -rn 's:^N(;.+)*\:([^;]*;){1} *([^;]*).*$:\3:p' "$file")" \
+      "$file"
+  done \
+  | sort \
+  | sed -r 's;^.*\t(.*/)([^/]+)$;\2;'
+}
+
+get_name() {
+  cfile="${_DATA}/vcard/$1"
+  sed -rn 's;^N(\;[^":]+|\;"[^"]+")*:([^\;]*)(\;[^\;]*)(\;[^\;]*)?(\;[^\;]*)?(\;[^\;]*)?\r?$;\5 \3 \4 \2 \6;;
+           tX; b; :X s;[,\; ]+; ;g; p;' "$cfile"
+}
+
+get_categories(){
+  cfile="${_DATA}/vcard/$1"
+  sed -rn 's;^CATEGORIES(\;[^":]+|\;"[^"]+")*:(.+)\r?$;\2;;
+           tX; b; :X
+           s;(^|[^\\]+)((\\\\)+),;\1\2\n;g;
+           s;(^|[^\\]),;\1\n;g; s;(^|[^\\]+)((\\\\)+),;\1\2\n;g;
+           s;(^|[^\\]),;\1\n;g; s;\\,;,;g; p;' "$cfile" \
+  | sort -u
+}
+
+list_categories() {
+  sort -u "$catfile" \
+  | sed -r '/^[\t ]*$/d'
+}
diff --git a/cards/categories/edit_categories.sh b/cards/categories/edit_categories.sh
new file mode 100755 (executable)
index 0000000..2c34d26
--- /dev/null
@@ -0,0 +1,31 @@
+#!/bin/zsh
+
+# Copyright 2015 - 2016 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+catfile="${_DATA}/mappings/categories"
+
+remove="${_POST[remove]}"
+newcat="${_POST[newcat]}"
+
+if [ "${_POST[add]}" = "add" ]; then
+  printf %s\\n "$newcat" >>"$catfile"
+elif [ -n "$remove" ]; then
+  sed -ri '/^'"${remove}"'$/d' $catfile
+fi
+
+redirect "?p=categories"
diff --git a/cards/categories/text_categories.sh b/cards/categories/text_categories.sh
new file mode 100755 (executable)
index 0000000..00c61ce
--- /dev/null
@@ -0,0 +1,22 @@
+# Copyright 2014, 2015 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+item_name[cat_remove]="&#45;"
+item_name[cat_add]="+"
+item_name[cat_newlabel]="neue Kategorie"
+item_name[cat_update]="Zuweisungen übernehmen"
+item_name[categories_label]="Kategorien"
diff --git a/cards/categories/update_categories.sh b/cards/categories/update_categories.sh
new file mode 100755 (executable)
index 0000000..c3b2e4c
--- /dev/null
@@ -0,0 +1,51 @@
+#!/bin/zsh
+
+# Copyright 2016 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+. "$_EXEC"/pages/categories.sh
+
+catfile="${_DATA}/mappings/categories"
+
+set_categories(){
+  card="${_DATA}/vcard/$1"
+  cats="$2"
+  cardcats="$(sed -nr 's;^CATEGORIES(\;[^":]+|\;"[^"]+")*:([^\r]+)\r?$;\2;p' "$card")"
+
+  debug "CARD: $card"
+  debug "CATS: $cardcats"
+  debug "NEW:  $cats"
+  if [ "$cats" != "$cardcats" ]; then
+    sed -ri '
+      /^CATEGORIES[;:]/d
+      /^END:VCARD *\r?$/iCATEGORIES:'"$cats"'\r
+    ' "${card}"
+  fi
+}
+
+listcards |while read card; do
+  cardcats=''
+  for n in "$card" "$card"{0..100}; do
+    if [ -z "${_POST[$n]}" ]; then
+      set_categories "$card" "$cardcats"
+      break
+    fi
+    cardcats="${cardcats}${cardcats:+,}${_POST[$n]}"
+  done
+done
+
+redirect "?p=categories"
diff --git a/cards/edit_card.sh b/cards/edit_card.sh
new file mode 100755 (executable)
index 0000000..61c58fe
--- /dev/null
@@ -0,0 +1,36 @@
+#!/bin/zsh
+
+# Copyright 2019 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+locktimeout=900
+. "$_EXEC"/session_lock.sh
+
+card="$(GET card |PATH)"
+cardfile="$_DATA/vcard/${card##*/}"
+filter="$(REF f)"
+order="$(REF o)"
+
+if tempfile="$(SLOCK "$cardfile" "$locktimeout")"; then
+  REDIRECT "/cards/?o=${order}&f=${filter}&e=${card}"
+elif [ -f "$tempfile" ]; then
+  SET_COOKIE session message="SESSLOCK"
+  REDIRECT "/cards/?o=${order}&f=${filter}#${card}"
+else
+  SET_COOKIE session message="EDITLOCK"
+  REDIRECT "/cards/?o=${order}&f=${filter}#${card}"
+fi
diff --git a/cards/export_card.sh b/cards/export_card.sh
new file mode 100755 (executable)
index 0000000..e9356a9
--- /dev/null
@@ -0,0 +1,27 @@
+#!/bin/zsh
+
+# Copyright 2014,2015 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+card="$(GET card |PATH)"
+cardfile="$_DATA/vcard/${card##*/}"
+
+if [ -f "$cardfile" ]; then 
+  . $_EXEC/cgilite/file.sh
+  FILE "$cardfile" "text/vcard/charset=utf-8"
+else
+fi
diff --git a/cards/filter_card.sh b/cards/filter_card.sh
new file mode 100755 (executable)
index 0000000..10ca51b
--- /dev/null
@@ -0,0 +1,42 @@
+#!/bin/zsh
+
+# Copyright 2014, 2017, 2019 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+if [ "$(POST choice)" = new_filter ]; then
+  filter="$(
+    seq 0 100 |while read n; do
+      filter_type="$(POST "filter_type${n}")"
+      filter_text="$(POST "filter_text${n}")"
+      [ ! "$filter_type" -a ! "$filter_text"  ] && break
+      if [ "$filter_type" = CATEGORIES ]; then
+        printf '^CATEGORIES:'
+        seq 0 $(POST_COUNT filter_cat$n) |while read m; do
+          printf '|%s' "$(POST filter_cat$n $m)"
+        done
+      else
+        printf '^%s:%s' "$filter_type" "$filter_text"
+      fi
+    done | sed -r \
+           's;\|+;\|;g;   s;\^+;\^;g;   s;:\|;:;g;
+            :X;   s;\^[^:]*:\^;\^;g;   /\^[^:]*:\^/bX;
+            s;^\^;;;   s;\^[^:]*:$;;;'
+  )"
+  REDIRECT "/cards/?o=$(POST order)&f=${filter}"
+else
+  REDIRECT '/cards/'
+fi
diff --git a/cards/index.cgi b/cards/index.cgi
new file mode 100755 (executable)
index 0000000..f1306f6
--- /dev/null
@@ -0,0 +1,32 @@
+#!/bin/sh
+
+if [ "$_PATH" = "/cards/cards.css" ]; then
+  . $_EXEC/cgilite/file.sh
+  FILE $_EXEC/cards/cards.css
+  return 0
+fi
+
+. $_EXEC/pdiread.sh
+. $_EXEC/cards/l10n.sh
+. $_EXEC/cards/widgets.sh
+. $_EXEC/cards/list.sh
+
+unescape() { [ $# = 0 ] && sed -E 's;\\(.);\1;g' || printf %s "$*" |sed -E 's;\\(.);\1;g'; }
+upcase=' y;abcdefghijklmnopqrstuvwxyzäöüé;ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÜÉ;; '
+
+filter="$(GET f)"
+order="$(GET o)"
+edit="$(GET e |PATH)"
+
+[ "$order" ] || order=firstname
+edit="${edit##*/}"
+
+{ w_filter_diag
+  printf '
+  [form class="newcard" action="/cards/new_card.sh" method="POST"
+    [button type="submit" %s]
+  ]' "$(l10n newcard)"
+
+  [ "$edit" ] && edit_card "$edit"
+  list_cards
+} | yield_page cards /cards/cards.css
diff --git a/cards/l10n.sh b/cards/l10n.sh
new file mode 100755 (executable)
index 0000000..044a6a1
--- /dev/null
@@ -0,0 +1,138 @@
+# Copyright 2014, 2016, 2019 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+l10n(){
+  local word
+  [ $# -eq 0 ] && read -r word || word="$1"
+  case $word in
+    PHOTO) printf %s "Foto";;
+    LOGO) printf %s "Logo";;
+    FN) printf %s "Voller Name";;
+    N) printf %s "Name";;
+    n_pre) printf %s "Titel";;
+    n_first) printf %s "Vorname";;
+    n_middle) printf %s "Mittelnamen";;
+    n_last) printf %s "Nachname";;
+    n_post) printf %s "Zusätze";;
+    NICKNAME) printf %s "Spitzname";;
+    SOUND) printf %s "Aussprache";;
+    GENDER) printf %s "Geschlecht";;
+    KIND) printf %s "Typ";;
+    TITLE) printf %s "Beruf";;
+    ROLE) printf %s "Position";;
+    ORG) printf %s "Organisation";;
+    MEMBER) printf %s "Mitglied";;
+    CATEGORIES) printf %s "Kategorien";;
+    ANNIVERSARY) printf %s "Jubiläum";;
+    BDAY) printf %s "Geburtstag";;
+    EMAIL) printf %s "E-Mail";;
+    TEL) printf %s "Telefon";;
+    teltype) printf %s "Anschlusstyp:";;
+    TYPE=HOME) printf %s "Privat";;
+    TYPE=WORK) printf %s "Geschäftlich";;
+    TYPE=CELL) printf %s "Mobil";;
+    TYPE=FAX) printf %s "Fax";;
+    IMPP) printf %s "Chat";;
+    ADR) printf %s "Anschrift";;
+    URL) printf %s "Webseite";;
+    LANG) printf %s "Sprache";;
+    NOTE) printf %s "Notiz";;
+  
+    RELATED) printf %s "Kontakte";;
+  
+    BEGIN) printf %s "";;
+    CALADRURI) printf %s "";;
+    CALURI) printf %s "";;
+    CLASS) printf %s "";;
+    CLIENTPIDMAP) printf %s "";;
+    END) printf %s "";;
+    FBURL) printf %s "";;
+    GEO) printf %s "";;
+    MAILER) printf %s "";;
+    NAME) printf %s "";;
+    PRODID) printf %s "";;
+    PROFILE) printf %s "";;
+    REV) printf %s "";;
+    SORT-STRING) printf %s "";;
+    SOURCE) printf %s "";;
+    TZ) printf %s "";;
+    UID) printf %s "";;
+    VERSION) printf %s "";;
+    XML) printf %s "";;
+  
+    X-HEALTH-INSURANCE) printf %s "Kran&shy;ken&shy;ver&shy;sich&shy;er&shy;ung";;
+    hi_from_list) printf %s "Aus Liste";;
+    hi_other) printf %s "Andere";;
+    hi_company) printf %s "Ver&shy;sich&shy;er&shy;ungs&shy;ge&shy;sell&shy;schaft";;
+    hi_number) printf %s "Ver&shy;sich&shy;er&shy;ten&shy;num&shy;mer";;
+    hi_status) printf %s "Ver&shy;sich&shy;er&shy;ten&shy;sta&shy;tus";;
+    X-HEALTH-INSURANCE-NOCONTRIB) printf %s "Zu&shy;zahl&shy;ungs&shy;be&shy;frei&shy;ung";;
+    X-CLIENT-REFERRAL) printf %s "Empfehl&shy;ung durch";;
+    prescriptions) printf %s "Verordnungen";;
+    new_prescription) printf %s "Neue Verordnung";;
+    no_icd) printf %s "Kein ICD Code";;
+  
+    X-ZACK-JOINDATE)  printf %s "An&shy;mel&shy;de&shy;da&shy;tum";;
+    X-ZACK-LEAVEDATE) printf %s "Ab&shy;mel&shy;de&shy;da&shy;tum";;
+    X-ZACK-JOINDATE_short)  printf %s "Anm.";;
+    X-ZACK-LEAVEDATE_short) printf %s "Abm.";;
+  
+    edit) printf %s "Bearbeiten";;
+    edit_categories) printf %s "Kategorien Bearbeiten";;
+    vcf_export) printf %s "Vcard Exportieren";;
+    control) printf %s "Aktionen";;
+    edit_update) printf %s "Daten übernehmen";;
+    edit_cancel) printf %s "Abbrechen";;
+    edit_delete) printf %s "Eintrag löschen";;
+    edit_addfieldtext) printf %s "Neues Feld";;
+    edit_addfield) printf %s "+";;
+    edit_deletefield) printf %s "X";;
+    filter_label) printf %s "Filter";;
+    filter_placeholder) printf %s "Begriffe zur Eingrenzung eingeben";;
+    filter_type) printf %s "Filtertyp";;
+    filter_order) printf %s "Sortierung";;
+    filter_any) printf %s "Alles";;
+    filter_name) printf %s "Name";;
+    filter_firstname) printf %s "Vorname";;
+    filter_lastname) printf %s "Nachname";;
+    filter_street) printf %s "Straße";;
+    filter_zip) printf %s "PLZ.";;
+    filter_TEL) printf %s "Telefon";;
+    filter_BDAY) printf %s "Geburtsjahr";;
+    filter_bdate) printf %s "Geburtsdatum";;
+    filter_course) printf %s "Kurs";;
+    filter_CATEGORIES) printf %s "Kategorien";;
+    filter_apply) printf %s "Filtern";;
+    filter_cancel) printf %s "Filter löschen";;
+    newcard) printf %s "Neuen Eintrag anlegen";;
+    course_attendance) printf %s "Kursteilnahme";;
+  
+    gender_none) printf %s "keine Angabe";;
+    gender_female) printf %s "Weiblich";;
+    gender_male) printf %s "Männlich";;
+    gender_other) printf %s "Sonstiges";;
+  
+    female) printf %s "&#x2640;";;
+    male) printf %s "&#x2642;";;
+    other) printf %s "&#x26A5;";;
+    none) printf %s "&#x26AA;";;
+
+
+    *) printf %s "$1";;
+  esac
+}
+
diff --git a/cards/list.sh b/cards/list.sh
new file mode 100755 (executable)
index 0000000..6321f0b
--- /dev/null
@@ -0,0 +1,178 @@
+#!/bin/sh
+
+. "${_EXEC}"/pdiread.sh
+
+edit_card(){
+  local cardfile="$_DATA/vcard/$1" 
+  local tempfile card
+
+  . $_EXEC/session_lock.sh
+
+  if ! tempfile="$(CHECK_SLOCK "$cardfile")"; then
+    printf '[div .message %s]' "$(l10n "This card is not set up for editing within this session.")"
+  else
+    card="$(pdi_load "$tempfile")"
+    cat <<-EOF
+       [form .card #${cardfile##*/} action="/cards/update_card.sh" method="POST"
+         [input type="hidden" name="tid" value="$(transid ${tempfile})"]
+         [div .section .basic $(
+           edit_item "$card" N GENDER
+           [ "$(pdi_count "$card" NICKNAME)" -gt 0 ] \
+           && edit_item "$card" NICKNAME
+           edit_item "$card" BDAY
+           edit_item "$card" X-ZACK-JOINDATE
+           [ "$(pdi_count "$card" X-ZACK-LEAVEDATE)" -gt 0 ] \
+           && edit_item "$card" X-ZACK-LEAVEDATE
+           card_item "$card" SOUND PHOTO LOGO
+         )]
+         [div .section .phone   $(edit_item "$card" TEL)]
+         [div .section .message $(
+           edit_item "$card" EMAIL
+           [ $(pdi_count "$card" IMPP) -gt 0 ] && edit_item "$card" IMPP
+           [ $(pdi_count "$card" URL ) -gt 0 ] && edit_item "$card" URL
+         )]
+         [div .section .address $(edit_item "$card" ADR)]
+         [div .section .note    $(edit_item "$card" NOTE)]
+         [div .section .attendance
+           [h3 $(l10n course_attendance) ] $(
+           for course in "$_DATA"/ical/*.ics; do
+             printf '[label [input type="checkbox" name="attendance" value="%s" %s] %s]' \
+                    "${course##*/}" \
+                    "$(grep -qF "${course##*/} ${cardfile##*/}" "$_DATA/mappings/attendance" \
+                       && printf 'checked="checked"'
+                    )" \
+                    "$(pdi_value "$(pdi_load "$course")" SUMMARY |unescape |HTML)"
+           done)
+           [h3 $(l10n CATEGORIES) ] $(
+           grep -xE '[^ ]+' "$_DATA"/mappings/categories |while read -r cat; do
+             printf '[label [input type="checkbox" name="attendance" value="%s" %s] %s]' \
+                    "$(HTML "$cat")" \
+                    "$(seq 1 $(pdi_count "$card" CATEGORIES) |while read c; do
+                      pdi_value "$card" CATEGORIES $c |grep -qxF "$cat" \
+                      && printf 'checked="checked"' && break
+                    done)" \
+                    "$(HTML "$cat")"
+           done)
+         ]
+         [div .control
+           [select .item name="newfield"
+             [option value="" disabled="disabled" selected="selected" $(l10n edit_addfieldtext)]
+             $(for f in NICKNAME EMAIL TEL IMPP ADR URL NOTE; do
+               printf '[option value="%s" %s] ' "$f" "$(l10n "$f")"
+             done)
+          ][button .item type="submit" name="action" value="addfield" $(l10n edit_addfield)]
+           [button .item type="submit" name="action" value="update"   $(l10n edit_update)]
+           [input type="checkbox" #delete] [label .item for="delete" $(l10n edit_delete)]
+             [button .item type="submit" name="action" value="delete"]
+           [button .item type="submit" name="action" value="cancel"   $(l10n edit_cancel)]
+         ]
+         [input type="hidden" name="UID" value="$(pdi_value "$card" UID |HTML)"]
+         [input type="hidden" name="card" value="${cardfile##*/}"]
+       ]
+       EOF
+  fi
+}
+
+print_card(){
+  local cardfile="$1"
+  local card="$(pdi_load "$cardfile")"
+  cat <<-EOF
+    [div .card #${cardfile##*/}
+      [div .section .basic . $(
+        card_item "$card" FN GENDER NICKNAME BDAY X-ZACK-JOINDATE X-ZACK-LEAVEDATE SOUND PHOTO LOGO
+      )]
+      [div .section .phone   . $(card_item "$card" TEL)]
+      [div .section .message . $(card_item "$card" EMAIL IMPP URL)]
+      [div .section .address . $(card_item "$card" ADR)]
+      [div .section .note    . $(card_item "$card" NOTE)]
+      [div .section .attendance [h3 $(l10n course_attendance) ] [ul .
+        $(grep -F "    ${cardfile##*/}" "$_DATA/mappings/attendance" |while read each discard; do
+          printf '[li [a .item .attendance href="/courses#%s" . %s]]' \
+                 "$each" \
+                 "$(pdi_value "$(pdi_load "$_DATA/ical/$each")" SUMMARY |unescape |HTML)"
+        done)]
+        $(card_item "$card" CATEGORIES)
+      ]
+      [div .control
+        [a .item href="/cards/edit_card.sh?card=${cardfile##*/}" $(l10n edit)]
+        [a .item href="/cards/export_card.sh?card=${cardfile##*/}" $(l10n vcf_export)]
+      ]
+    ]
+       EOF
+}
+
+print_cards(){
+  local cardfile cachefile date size name ldate=0 lsize lname
+
+  while read cardfile; do
+    cachefile="${_DATA}/cache/${cardfile##*/}.cache"
+    # if [ -s "$cachefile" -a "$cachefile" -nt "$cardfile" \
+    #                      -a "$cachefile" -nt "${_EXEC}/cards" ]; then
+    if [ -s "$cachefile" -a "$cachefile" -nt "$cardfile" ]; then
+      cat "$cachefile"
+    else
+      print_card "$cardfile" |tee "$cachefile"
+    fi
+  done
+}
+
+filter_cards(){
+  local filter f fex='x;p;'
+
+  filter="$(printf %s "${filter}" \
+            | sed -E 's;[]\/\(\)\\\$\?\.\+\*\;\[\{\}];\\\\&;g;
+                      '"$upcase"
+           )^"
+
+  while [ "$filter" ]; do
+    f="${filter%%^*}" filter="${filter#*^}"
+    case $f in
+      '') break
+        ;;
+      ANY:*) fex="/\n.*(\;[^:]*)?:.*(${f#*:}).*\r?\n/{${fex}}"
+        ;;
+      NAME:*) fex="/\n(N|FN|NICKNAME)(\;[^:]*)?:.*(${f#*:}).*\r?\n/{${fex}}"
+        ;;
+      STREET:*|ZIP:*) fex="/\nADR(\;[^:]*)?:.*(${f#*:}).*\r?\n/{${fex}}"
+        ;;
+      *) fex="/\n${f%%:*}(\;[^:]*)?:.*(${f#*:}).*\r?\n/{${fex}}"
+        ;;
+    esac
+  done
+
+  for cardfile in "${_DATA}"/vcard/*.vcf; do
+    printf '%s\n' "$cardfile"
+    cat "$cardfile"
+  done \
+  | sed -En ':X; /\nEND:VCARD\r?$/!{ N; bX; }; h; s;\n.*$;;; x; s;^[^\n]+\n;;;
+             '"$upcase""$fex"
+}
+
+order_cards() {
+  local cardfile card
+
+  while read cardfile; do
+    card="$(pdi_load "$cardfile")"
+
+    case $order in
+      firstname)
+        printf '%s     %s\n' "$(pdi_value "$card" FN)" "$cardfile"
+        ;;
+      lastname)
+        printf '%s     %s\n' "$(pdi_value "$card" N || pdi_value "$card" FN)" "$cardfile"
+        ;;
+      bdate)
+        printf '%s     %s\n' "$(pdi_value "$card" BDAY || printf 0000-00-00)" "$cardfile"
+        ;;
+    esac
+  done \
+  | sort \
+  | sed -E 's;^.*\t;;g'
+}
+
+list_cards(){
+  filter_cards \
+  | order_cards \
+  | grep -xvF "$edit" \
+  | print_cards
+}
diff --git a/cards/new_card.sh b/cards/new_card.sh
new file mode 100755 (executable)
index 0000000..edc7bc9
--- /dev/null
@@ -0,0 +1,47 @@
+#!/bin/zsh
+
+# Copyright 2014, 2019 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+filter="$(REF f)"
+order="$(REF o)"
+
+uid="$(timeid)$(randomid)"  # 32 Octets UID, starting with timestamp
+card="${uid}.vcf"
+
+mkdir -p "${_DATA}/lock/vcard/"
+lockdir="${_DATA}/lock/vcard/${card}/"
+lockfile=${lockdir}/${SESSION_ID}
+
+if mkdir "$lockdir"; then
+  cat >"$lockfile" <<-EOF
+       BEGIN:VCARD
+       VERSION:4.0
+       N:;;;;
+       BDAY:
+       TEL:
+       EMAIL:
+       ADR:
+       NOTE:
+       UID:${uid}
+       END:VCARD
+       EOF
+  REDIRECT "/cards/?o=${order}&f=${filter}&e=${card}"
+else
+  SET_COOKIE session message="EDITLOCK"
+  REDIRECT "/cards/?o=${order}&f=${filter}"
+fi
diff --git a/cards/update_card.sh b/cards/update_card.sh
new file mode 100755 (executable)
index 0000000..34db10c
--- /dev/null
@@ -0,0 +1,134 @@
+#!/bin/zsh
+
+# Copyright 2014, 2016, 2019 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+. "$_EXEC/pdiread.sh"
+. "$_EXEC/session_lock.sh"
+
+unset filter order card action newfield
+unset cardfile attfile tempfile
+unset vcf field cnt delete_key
+
+filter="$(REF f)"
+order="$(REF o)"
+
+card="$(POST card |PATH)"
+cardfile="$_DATA/vcard/${card##*/}"
+attfile="$_DATA/mappings/attendance"
+
+action="$(POST action)"
+newfield="$(POST newfield |grep -m 1 -xE '[A-Z][A-Z0-9-]*')"
+
+if printf '%s\n' "$action" |grep -qxE 'addfield [A-Z][A-Z0-9]*'; then
+  newfield="${action##* }"
+  action=addfield
+fi
+
+if ! tempfile=$(CHECK_SLOCK "$cardfile"); then
+  SET_COOKIE 0 message="NO VALID FILE LOCK"
+  REDIRECT "/cards/?o=${order}&f=${filter}&e=${card}"
+  exit 0
+elif [ "$(POST tid)" != "$(transid "$tempfile")" ]; then
+  SET_COOKIE 0 message="INVALID TRANSACTION ID"
+  REDIRECT "/cards/?o=${order}&f=${filter}&e=${card}"
+  exit 0
+fi
+
+vcf_escape(){
+  for each in "$@"; do
+    printf %s\\n "$each" \
+    | sed -E ':X;$!{N;bX}; s;\r\n;\n;g; s;([;,\\]);\\\1;g; s;\n;\\n;g;'
+  done \
+  | sed -E ':X;$!{N;bX}; s;\n;\;;g'
+}
+
+# [ "${_POST[hi_select]}" = "list" ] || _POST[hi_company]="${_POST[hi_other]}"
+# [ -n "${_POST[hi_company]}${_POST[hi_number]}${_POST[hi_status]}" ] \
+# && _POST[X-HEALTH-INSURANCE]="$(vcf_escape "${_POST[hi_company]}" "${_POST[hi_number]}" "${_POST[hi_status]}")"
+
+vcf="$(pdi_load "$cardfile")"
+
+vcf="$(pdi_update_value "$vcf" N 1 "$(vcf_escape "$(POST 1N)" "$(POST 2N)" "$(POST 3N)" "$(POST 4N)" "$(POST 5N)")")"
+
+for field in $(POST_KEYS |grep -xE '[A-Z][A-Z0-9-]*'); do
+  for cnt in $(seq 1 $(POST_COUNT "$field")); do
+    case "$field" in
+      # (TEL)
+      #   printf '%s;TYPE=%s:%s\r\n' "${field}" "${_POST[phonetype${key#TEL}]}" "$(vcf_escape "$(POST "$field" "$cnt")")"
+      #   ;;
+      TEL)
+         vcf="$(pdi_update_attrib "$vcf" TEL $cnt TYPE="$(POST teltype $cnt |grep -Exm1 'HOME|WORK|CELL|FAX')")"
+         vcf="$(pdi_update_value "$vcf" "$field" "$cnt" "$(vcf_escape "$(POST "$field" "$cnt")")")"
+         ;;
+      *)
+         vcf="$(pdi_update_value "$vcf" "$field" "$cnt" "$(vcf_escape "$(POST "$field" "$cnt")")")"
+        ;;
+    esac
+done; done
+
+# delete fields, first mark for deletion using delete_key
+# this way the field enumeration is preserved during the process
+# finally filter marked lines
+delete_key="$(randomid)"
+for delete in $(POST_KEYS |grep -xE '[A-Z][A-Z0-9-]*_delete_[0-9]+'); do
+  f="${delete%%_*}"; c="${delete##*_}";
+  [ "$(POST "$delete")" = "true" ] && vcf="$(pdi_update_value "$vcf" "$f" "$c" "delete=${delete_key}")"
+done
+vcf="$(printf '%s\n' "$vcf" |sed -E "/^[^:]+:delete=${delete_key}\$/d")"
+
+if [ "$action" = addfield ]; then
+  vcf="$(pdi_update_value "$vcf" "$newfield" $(( $(pdi_count "$vcf" "$newfield") + 1 )) '')"
+fi
+printf '%s' "$vcf" |grep -vx '' >"$tempfile"
+
+case "$action" in
+  addfield)
+    REDIRECT "/cards/?o=${order}&f=${filter}&e=${card}"
+    ;;
+  update)
+    # attendance=()
+    # for att in attendance attendance{0..100}; do
+    #   [ -n "${_POST[$att]}" ] && attendance+=("${_POST[$att]}")
+    # done
+    # sed -rn 's:^(.+)'$card'$:\1:p' "$attfile" |while read course; do
+    #   touch "$_DATA/ical/$course"
+    # done
+    # sed -i -r '/^(.+)\t'$card'$/d' "$attfile"
+    # for each in $attendance; do
+    #   echo "$each\t$card"
+    # done >>"$attfile"
+    # sed -rn 's:^(.+)'$card'$:\1:p' "$attfile" |while read course; do
+    #   touch "$_DATA/ical/$course"
+    # done
+
+    cp "$tempfile" "$cardfile"
+    RELEASE_SLOCK "$cardfile"
+    REDIRECT "/cards/?o=${order}&f=${filter}#${card}"
+    ;;
+  cancel)
+    RELEASE_SLOCK "$cardfile"
+    [ -f "$cardfile" ] \
+    && REDIRECT "/cards/?o=${order}&f=${filter}#${card}" \
+    || REDIRECT "/cards/?o=${order}&f=${filter}"
+    ;;
+  delete)
+    rm "$cardfile"
+    RELEASE_SLOCK "$cardfile"
+    REDIRECT "/cards/?o=${order}&f=${filter}"
+    ;;
+esac
diff --git a/cards/widgets.sh b/cards/widgets.sh
new file mode 100755 (executable)
index 0000000..5fbf125
--- /dev/null
@@ -0,0 +1,257 @@
+# Copyright 2014 - 2019 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+list_categories() {
+  grep -vxE '^[        ]*$' "${_DATA}/mappings/categories"
+}
+
+w_filter_item() {
+n=$3
+cat <<EOF
+  [fieldset .item
+    [legend $(l10n filter_item):]
+
+    $(for field in any name street zip TEL BDAY CATEGORIES; do
+      printf '[input id="%s%i" type="radio" name="filter_type%i" value="%s" %s]
+              [label for="%s%i" %s ]' \
+              "$field" "$n" "$n" "$field" "$([ "$1" = "$field" ] && printf checked )" \
+              "$field" "$n" "$(l10n filter_$field)"
+    done)
+    [input type="text" name="filter_text$n" value="$([ "$1" = CATEGORIES ] || HTML "$2")" placeholder="$(l10n filter_placeholder)"]
+    [fieldset .categories
+      $(list_categories | while read cat; do
+          printf '[label [checkbox "filter_cat%i" "|%s" %s] %s ]' \
+                 "$n" "$(HTML "$cat")" \
+                 "$(printf %s "$cat" |grep -qxEe "$2" && printf checked )" \
+                 "$(HTML "$cat")"
+      done)
+      [a href="/cards/categories.sh" $(l10n edit_categories)]
+    ]
+  ]
+EOF
+}
+
+w_filter_diag(){
+  cat <<EOF
+  [form .filter action="/cards/filter_card.sh" method="POST"
+    [h1 $(l10n filter_label)]
+    [input type="hidden" name="page" value="cards"]
+  
+    $(n=0; filter="${filter}^"
+      while [ "${filter#^}" ]; do
+        fil="${filter%%^*}" filter="${filter#*^}"
+        w_filter_item "${fil%%:*}" "${fil#*:}" $n
+        n=$((n + 1))
+      done
+      w_filter_item any '' $n
+    )
+  
+    [fieldset class="order"
+      [legend $(l10n filter_order):]
+      [label [radio "order" "firstname" $( [ "$order" = firstname ] && printf checked )] $(l10n filter_firstname)]
+      [label [radio "order" "lastname"  $( [ "$order" = lastname  ] && printf checked )] $(l10n filter_lastname)]
+      [label [radio "order" "bdate"     $( [ "$order" = bdate     ] && printf checked )] $(l10n filter_bdate)]
+    ]
+    [button type="submit" name="choice" value="new_filter" $(l10n filter_apply)]
+    [button type="submit" name="choice" value="del_filter" $(l10n filter_cancel)]
+  ]
+EOF
+}
+
+# listcards |grep ${edit:+-v} "$edit" \
+# | while read card; do
+#   "${_EXEC}"/cgilite/html-sh.sed <<-ENDCARD
+#      [div #${card} .card
+#        $(view_card "$card")[!--
+#        --][div .control
+#          [a "?action=edit_card&card=${card}" .item $(l10n edit)]
+#          [a "?action=export_vcard&card=${card}".item $(l10n vcf_export)]
+#          ${profile_medical:+[a "?action=new_prescription&client=${card}" .item $(l10n new_prescription)]}
+#      ]]
+#      ENDCARD
+# done
+
+#!/bin/sh
+
+card_item(){
+  local card="$1"
+  local item cnt c
+  shift 1
+
+  for item in $@; do
+    cnt="$(pdi_count "$card" "$item")"
+
+    case $item in
+      FN) printf '[h2 .item .FN &shy;%s]' "$(pdi_value "$card" FN |unescape |HTML)"
+        ;;
+      GENDER) printf '[span .item .GENDER &shy;%s]' "$(pdi_value "$card" GENDER |l10n)"
+        ;;
+      NICKNAME) seq 1 $cnt |while read c; do
+          printf '[span .item .NICKNAME &shy;aka. "%s"]' \
+                 "$(pdi_value "$card" NICKNAME $c |unescape |HTML)"
+        done
+        ;;
+      X-ZACK-JOINDATE|X-ZACK-LEAVEDATE) if [ $cnt -gt 0 ]; then
+          printf '[span .item .%s [b %s:] %s]' \
+                 "$item" "$(l10n "${item}_short")" \
+                 "$(pdi_value "$card" "$item" |HTML)"
+        fi
+        ;;
+      BDAY)
+       [ $cnt -gt 0 ] && printf '[span .item .BDAY [b *:] %s]' \
+                                "$(pdi_value "$card" BDAY |grep -xE '[0-9-]+')"
+        ;;
+      SOUND)
+        [ $cnt -gt 0 ] && printf '[audio .item .SOUND controls="controls"
+                                    [source type="audio/ogg" src="data:audio/ogg;base64,%s"]
+                                  ]' \
+                                  "$(pdi_value "$card" SOUND |grep -xE '[a-zA-Z0-9/+=]+')"
+        ;;
+      PHOTO|LOGO)
+        [ $cnt -gt 0 ] && printf '[img .item .%s src="data:image/%s;base64,%s"]' "$item" \
+                                 "$(pdi_attrib "$card" "$item" |sed -E 's;^(.*;)?TYPE="?(.+)"?(;.*)?$;\2;')" \
+                                 "$(pdi_value "$card" "$item" |grep -xE '[a-zA-Z0-9/+=]+')"
+        ;;
+      EMAIL) 
+        [ $cnt -gt 0 ] && printf '[h3 %s]' "$(l10n EMAIL)"
+        seq 1 $cnt |while read c; do
+          printf '[a .item .EMAIL href="mailto:%s" &shy;%s]' \
+                 "$(pdi_value "$card" EMAIL $c |unescape |HTML)" \
+                 "$(pdi_value "$card" EMAIL $c |unescape |HTML)"
+        done
+        ;;
+      TEL)
+        [ $cnt -gt 0 ] && printf '[h3 %s]' "$(l10n TEL)"
+        seq 1 $cnt |while read c; do
+          teltype="$(pdi_attrib "$card" TEL $c TYPE)"
+          [ "$teltype" ] \
+          && printf '[span .item .TEL [span .type &shy;%s:] %s]' \
+                    "$(l10n "TYPE=$teltype" |HTML)" \
+                    "$(pdi_value "$card" TEL $c |unescape |HTML)" \
+          || printf '[span .item .TEL &shy;%s]' \
+                    "$(pdi_value "$card" TEL $c |unescape |HTML)"
+        done
+        ;;
+      *)[ $cnt -gt 0 ] && printf '[h3 %s]' "$(l10n "$item")"
+        seq 1 $cnt |while read c; do
+          printf '[span .item .%s &shy;%s]' "$item" \
+                 "$(pdi_value "$card" "$item" $c |unescape |HTML)"
+        done
+        ;;
+    esac
+  done
+}
+
+edit_item(){
+  local card="$1"
+  local item cnt c
+  shift 1
+
+  for item in $@; do
+    cnt="$(pdi_count "$card" "$item")"
+    [ $cnt -lt 1 ] && cnt=1
+
+    case $item in
+      N)N="$(pdi_value "$card" N)"
+        if [ "$N" ]; then
+          IFS=\; read n1 n2 n3 n4 n5 <<-EOF
+               $N
+               EOF
+        else
+         N="$(pdi_value "$card" FN |unescape)"
+          n1="${N%%[a-z]*}" n1="${N#$n1}"
+          [ "$n1" ] || n1="${N##* }"
+          n2="${N%$n1}"
+        fi
+        printf '
+        [h3 %s]
+        [input .item .N name="4N" placeholder="%s" value="%s"]
+        [input .item .N name="2N" placeholder="%s" value="%s"]
+        [input .item .N name="1N" placeholder="%s" value="%s"]
+        [input .item .N name="5N" placeholder="%s" value="%s"]
+        ' "$(l10n "$item")" \
+        "$(l10n n_pre)"   "$(HTML "$n4")" \
+        "$(l10n n_first)" "$(HTML "$n2" "$n3")" \
+        "$(l10n n_last)"  "$(HTML "$n1")" \
+        "$(l10n n_post)"  "$(HTML "$n5")"
+        ;;
+      GENDER)
+        gender="$(pdi_value "$card" GENDER)"
+        printf '
+        [select .item .GENDER name="GENDER"
+          [option value="" disabled="disabled" %s %s]
+          [option value="female" %s %s]
+          [option value="male"   %s %s]
+          [option value="other"  %s %s]
+          [option value="none"   %s %s]
+        ]\n' \
+        "$([ "$gender" = ''       ] && printf 'selected="selected"')" "$(l10n GENDER)" \
+        "$([ "$gender" = 'female' ] && printf 'selected="selected"')" "$(l10n gender_female)" \
+        "$([ "$gender" = 'male'   ] && printf 'selected="selected"')" "$(l10n gender_male)" \
+        "$([ "$gender" = 'other'  ] && printf 'selected="selected"')" "$(l10n gender_other)" \
+        "$([ "$gender" = 'none'   ] && printf 'selected="selected"')" "$(l10n gender_none)"
+        ;;
+      BDAY|X-ZACK-JOINDATE|X-ZACK-LEAVEDATE)
+        printf '[h3 %s]
+        [input .item .%s name="%s" value="%s" placeholder="YYYY-MM-DD"]' \
+        "$(l10n "$item")" "$item" "$item" "$(pdi_value "$card" "$item" |grep -xE '[0-9-]+')"
+        ;;
+      ADR|NOTE)
+        printf '[h3 %s]' "$(l10n "$item")"
+        seq 1 $cnt |while read c; do
+          printf '[checkbox "%s_delete_%i" "true" .delete #%s_delete_%i][label for="%s_delete_%i" %s]' \
+            "$item" $c "$item" $c "$item" $c "$(l10n delete)"
+          printf '<textarea class="item %s" name="%s">%s</textarea>' \
+            "$item" "$item" "$(pdi_value "$card" "$item" $c |unescape |HTML)"
+        done
+        printf '[button type="submit" name="action" value="addfield %s" %s ]' "$item" "$(l10n edit_addfield)"
+        ;;
+      TEL) printf '[h3 %s]' "$(l10n "$item")"
+        seq 1 $cnt |while read c; do
+          printf '[checkbox "%s_delete_%i" "true" .delete #%s_delete_%i][label for="%s_delete_%i" %s]' \
+            "$item" $c "$item" $c "$item" $c "$(l10n delete)"
+          teltype="$(pdi_attrib "$card" TEL $c TYPE)"
+          printf '[select .item .teltype name="teltype"
+                    [option value="" disabled="disabled" %s %s]
+                    [option value="HOME" %s %s]
+                    [option value="WORK" %s %s]
+                    [option value="CELL" %s %s]
+                    [option value="FAX"  %s %s]
+                  ]\n' \
+                  "$([ "$teltype" = ''     ] && printf 'selected="selected"')" "$(l10n teltype)" \
+                  "$([ "$teltype" = 'HOME' ] && printf 'selected="selected"')" "$(l10n TYPE=HOME)" \
+                  "$([ "$teltype" = 'WORK' ] && printf 'selected="selected"')" "$(l10n TYPE=WORK)" \
+                  "$([ "$teltype" = 'CELL' ] && printf 'selected="selected"')" "$(l10n TYPE=CELL)" \
+                  "$([ "$teltype" = 'FAX'  ] && printf 'selected="selected"')" "$(l10n TYPE=FAX)"
+
+          printf '[input .item .%s name="%s" value="%s" placeholder="%s"]' \
+            "$item" "$item" "$(pdi_value "$card" "$item" $c |unescape |HTML)" "$(l10n "$item")"
+        done
+        printf '[button type="submit" name="action" value="addfield %s" %s ]' "$item" "$(l10n edit_addfield)"
+        ;;
+      *)printf '[h3 %s]' "$(l10n "$item")"
+        seq 1 $cnt |while read c; do
+          printf '[checkbox "%s_delete_%i" "true" .delete #%s_delete_%i][label for="%s_delete_%i" %s]' \
+            "$item" $c "$item" $c "$item" $c "$(l10n delete)"
+          printf '[input .item .%s name="%s" value="%s" placeholder="%s"]' \
+            "$item" "$item" "$(pdi_value "$card" "$item" $c |unescape |HTML)" "$(l10n "$item")"
+        done
+        printf '[button type="submit" name="action" value="addfield %s" %s ]' "$item" "$(l10n edit_addfield)"
+        ;;
+    esac
+  done
+}
similarity index 100%
rename from cgilite.sh
rename to cgilite/cgilite.sh
similarity index 100%
rename from file.sh
rename to cgilite/file.sh
similarity index 100%
rename from html-sh.sed
rename to cgilite/html-sh.sed
similarity index 100%
rename from logging.sh
rename to cgilite/logging.sh
similarity index 100%
rename from session.sh
rename to cgilite/session.sh
similarity index 100%
rename from storage.sh
rename to cgilite/storage.sh
diff --git a/courses/course_print.sh b/courses/course_print.sh
new file mode 100755 (executable)
index 0000000..72ab8b8
--- /dev/null
@@ -0,0 +1,63 @@
+# Copyright 2014 - 2016 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+. ${_EXEC}/templates/text_cards.sh
+
+echo -E '
+\documentclass[landscape,10pt]{article}
+\usepackage[utf8x]{inputenc}
+\usepackage{ngerman}
+\usepackage{eurosym}
+\usepackage[landscape,margin=0.25in]{geometry}
+\usepackage{longtable}
+
+\begin{document}
+
+\section*{Teilnehmende}
+\begin{longtable}{|p{60mm}|l|p{50mm}|p{80mm}|}
+\hline
+  \textbf{'"$(l10n N)"'} &
+  \textbf{'"$(l10n BDAY)"'} &
+  \textbf{'"$(l10n TEL)"'} &
+  \textbf{'"$(l10n NOTE)"'} \\
+\hline
+\hline
+\endhead
+'"$(
+list_attendance "$course" |sort -k 2 |while read line; do
+  cardfile="$(echo "$line" |cut -d\  -f1)"
+  list_attendee "$cardfile" |sed -r 's:$:\\\\[3ex] \\hline:'
+done
+)"'
+\end{longtable}
+
+\newpage
+
+\section*{Termine}
+\begin{longtable}{|p{60mm}|c|c|c|c|c|c|c|c|c|c|}
+\hline
+ '"$(get_dates)"' \\
+\hline
+\hline
+\endhead
+'"$(
+tex_clean "$(list_attendance "$course")" |sort -k 2 | debug |sed -r 's:^[0-9a-z\.]+ (.+) \(\*[0-9]{4}\)$:\1:;s:$: \& \& \& \& \& \& \& \& \& \& \\\\[3ex] \\hline:'
+)"'
+\end{longtable}
+
+\end{document}
+'
diff --git a/courses/courses.css b/courses/courses.css
new file mode 100644 (file)
index 0000000..a8a8cc3
--- /dev/null
@@ -0,0 +1,195 @@
+/*
+# Copyright 2014 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+*/
+
+.sort {
+  display: block;
+  border: solid 1px;
+  border-radius: 4px 4px 0 0 ;
+  margin: .5em 2em .25em 2em;
+  padding: .25em 2ex .25em 2ex;
+  background: #EFF;
+}
+.sort .label {
+  font-weight: bold;
+}
+
+.newcourse {
+  display: block;
+  border: solid 1px;
+  border-radius: 0 0 4px 4px;
+  margin: .25em 2em 1em 2em;
+  padding: .25em 2ex .25em 2ex;
+  background: #EFF;
+}
+
+.course {
+  display: block;
+  position: relative;
+  border: solid 1px;
+  margin: .25em 2em;
+  padding: 0 20ex 0 0;
+  overflow: auto;
+  background: #FFF;
+  min-height: 9em;
+}
+.course .control {
+  display: inline-block;
+  position: absolute;
+  top 0; right: 0;
+  text-align: right;
+  background: #EEF;
+  margin: 0;
+  height: 100%;
+  width: 20ex;
+  max-width: 33%;
+  padding: .25em 1ex;
+}
+.course .control .item {
+  display: inline-block;
+  vertical-align: top;
+  width: 100%;
+  color: #008;
+  margin: .25em 0 0 0;
+  border: 1px solid black;
+  background: #FFF;
+}
+.course .control a.item {
+  border-right: 3px double;
+  padding: .125em 1ex;
+}
+.course .control a.item:hover{
+  border-right: 1px solid;
+}
+
+.course .control .item[name=newfield] { width: 85%;}
+.course .control .item[value=addfield] { width: 15%;}
+
+.course .section {
+  display: inline-block;
+  vertical-align: top;
+  float: left;
+  width: 20ex;
+  min-width: 16.5%;
+  min-height: 6em;
+  margin: .125em 0 .5ex 0;
+  padding: 0 1ex .25em 1ex;
+}
+.course .section:nth-of-type(2n){ background: #EEE;}
+
+.course .section .item {
+  display: block;
+  max-width: 100%;
+  word-wrap: break-word;
+}
+
+.course .section h2, .course .section h3 {
+  font-size: 1em;
+  display: block;
+  font-weight: bold;
+  margin: .25em -.5ex .25em -.5ex;
+  border-style: none none solid none;
+  border-width: 1px;
+}
+.course .section h2 {
+  font-size: 1.2em;
+  margin-top: 0;
+}
+
+.course .attendance {
+  min-width: 66%;
+}
+.course .attendance a {
+  display: inline-block;
+  vertical-align: top;
+  min-width: 13.5ex;
+  width: 19%;
+  color: #008;
+  border-style: solid;
+  border-width: 1px;
+  margin: .25em .125ex .25em .125ex;
+  padding: .125em 1ex .125em 1ex;
+  background: #EFF;
+}
+
+.course .dtstart { width: 37ex; }
+.course .dtstart .DTSYEAR,
+.course .dtstart .DTSMONTH,
+.course .dtstart .DTS{
+  font-size: .875em;
+  margin: .25em .25ex 1em .25ex;
+  display: inline-block;
+  text-align: center;
+  font-weight: bold;
+}
+
+.course .dtstart .DTSYEAR{ width: 11.5ex; }
+.course .dtstart .DTSMONTH{ width: 18.5ex; }
+.course .dtstart .DTS{ width: 5.5ex; }
+
+.course .dtstart .DTSCAL,
+.course .dtstart .DTSCALHEAD {
+  display: inline-block;
+  overflow: hidden;
+  text-align: center;
+  width: 5ex;
+  margin: 0;
+}
+.course .dtstart .DTSCALHEAD {
+  font-weight: bold;
+  border-top: 1px solid;
+  border-bottom: 1px solid;
+}
+
+.course .dtstart input[name=DTSDAY] { display: none; margin: 0; height: 0; width: 0; }
+.course .dtstart input[name=DTSDAY]:checked + label.DTSCAL { font-weight: bold; border: solid 1px; }
+
+.course .dtstart .DTSTIME{
+  display: inline-block;
+  width: 10ex;
+  font-weight: bold;
+}
+
+.course .recur{ width: 37ex; }
+.course .section select,
+.course .section button,
+.course .section input {
+  border: 1px solid;
+  margin: .5em 0;
+}
+
+.course .select_attendance {
+  max-height: 17em;
+  overflow-y: scroll;
+}
+.course .select_attendance label{
+  display: block;
+  position: relative;
+  margin: 0; padding: 0;
+  padding-left: 3ex;
+}
+.course .select_attendance label input{
+  position: absolute;
+  margin: 0; padding: 0;
+  margin-left: -3ex;
+  top: 0;
+}
+
+.course .section input[type="text"] { width: 100%; }
+.course .section textarea.COMMENT { min-height: 6em; width: 100%; overflow: auto; }
+
diff --git a/courses/courses.sh b/courses/courses.sh
new file mode 100755 (executable)
index 0000000..93e8261
--- /dev/null
@@ -0,0 +1,171 @@
+#!/bin/zsh
+
+# Copyright 2014, 2016, 2017 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+[ -z "${_GET[order]}" ] && _GET[order]=DOW
+
+listcards() {
+  ls -1 ${_DATA}/vcard/*vcf 2>/dev/null |while read file; do
+    fn=$(sed -rn 's:^N(;.+)*\:([^;]*;){1} *([^;]*).*$:\3:p' "$file")
+    echo "$fn\t$file"
+  done |sort |sed -r 's:^.*\t(.*/)([^/]+)$:\2:'
+}
+
+
+listcourses() {
+  case "${_GET[filtertype]}" in
+    *) ls -1 ${_DATA}/ical/*ics 2>/dev/null
+      ;;
+  esac |case "${_GET[order]}" in
+    DOW)
+      while read file; do
+        icstime="$(sed -rn 's:^DTSTART\:(TZID=.*\:)?([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})Z?\r$:\2-\3-\4 \5\:\6\:\7:p' "$file")"
+        echo "$(date -d "$icstime" "+%u %H%M%S")\t$file"
+      done
+      ;;
+    TOD)
+      while read file; do
+        icstime="$(sed -rn 's:^DTSTART\:(TZID=.*\:)?([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})Z?\r$:\2-\3-\4 \5\:\6\:\7:p' "$file")"
+        echo "$(date -d "$icstime" +%H%M%S)\t$file"
+      done
+      ;;
+    *)
+      sed -r 's:^.*$:x\t&:'
+      ;;
+  esac |sort |sed -r 's:^.*\t(.*/)([^/]+)$:\2:'
+}
+
+list_attendance() {
+  id="$1"
+  sed -rn 's:'$id'\t(.+)$:\1:p' "$_DATA/mappings/attendance" |while read each; do
+    card="$_DATA/vcard/$each"
+    if [ -r "$card" ]; then
+      aname="$(sed -rn '
+          /^N(;.+)*:/{
+          h;
+          s;^N(\;.+)*:([^\;]*\;){3} *([^\;]*).*$;\3;p; g;
+          s;^N(\;.+)*:([^\;]*\;){1} *([^\;]*).*$;\3;p; g;
+          s;^N(\;.+)*:([^\;]*\;){2} *([^\;]*).*$;\3;p; g;
+          s;^N(\;.+)*:([^\;]*\;){0} *([^\;]*).*$;\3;p; g;
+          s;^N(\;.+)*:([^\;]*\;){4} *([^\;]*).*$;\3;p; g;
+          }' "$card" \
+        | sed -r ':X;N;$!bX; s;([\;\n\r,]| )+; ;g;; s;^ +| +$;;g;'
+      )"
+      fname="$(sed -rn 's;^FN(\;.+)*:(.+)\r?$;\2;g; s;([\;\n,]| )+; ;g;; s;^ +| +$;;g;' "$card")"
+      nname="$(sed -rn 's;^NICKNAME(\;.+)*:(.+)\r?$;\2;g; s;([\;\n,]| )+; ;g;; s;^ +| +$;;g;' "$card")"
+      byear="$(sed -rn 's:^BDAY(\;.+)*\:([0-9]{4})(-[0-9][0-9]){2}.*$:\2:p' "$_DATA/vcard/$each")"
+
+      printf '%s %s (*%04i)\n' "$each" "${aname:-${fname:-${nname}}}" "$byear"
+    fi
+  done
+}
+
+course_mail() {
+  id="$1"
+  coursemail=""
+  sed -rn 's:'$id'\t(.+)$:\1:p' "$_DATA/mappings/attendance" |while read each; do
+    coursemail="$coursemail$(sed -rn 's:^EMAIL(;.+)*\:(.+)\r$:\2,:p' "$_DATA/vcard/$each")"
+  done
+  echo "$coursemail"
+}
+
+ics_parse() {
+  unset key
+  sed -r ':X;N;$!bX; s;\r\n[ \t];;g; s;\r\n;\n;g;' "$1" \
+  | sed -rn '
+    # === turn property names to upper case, strip group names ===
+    h; s;^([^;:]+);;;
+    x; s;^([^;:\.]+\.)?([^;:]+).*$;\2;;
+    y;abcdefghijklmnopqrstuvwxyz;ABCDEFGHIJKLMNOPQRSTUVWXYZ;
+    G; s;\n;;;
+
+    /^([A-Z0-9-]+)((\;[A-z0-9-]+=?|\;[A-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):(.*)$/{
+      h;
+      s;^([A-Z0-9-]+)((\;[A-z0-9-]+=?|\;[A-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):(.*)$;key='\''\1'\'';;
+      H; g;
+
+      s;^([A-Z0-9-]+)((\;[A-z0-9-]+=?|\;[A-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):([^\n]*)\n.*$;\2;;
+      s;'\'';'\'\\\\\'\'';g
+      s;\;([A-Z0-9-]+)(=|=(([^\;,:"]+|"[^"]+")(,[^\;,:"]+|,"[^"]+")*))?;\ntag[\1]='\''\3'\'';g
+      s;^\n+;;; s;\n+$;;;
+      /^.+$/H; g;
+
+      s;^([A-Z0-9-]+)((\;[A-z0-9-]+=?|\;[A-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):([^\n]*)\n.*$;\6;;
+      # :X s;((^|[^\\])(\\\\)*)[;,];\1\n;g; s;((^|[^\\])(\\\\)*)\\([;,]);\4;g; tX;
+      s;'\'';'\'\\\\\'\'';g
+      s;^.*$;value='\''&'\'';
+      H; g;
+
+      s;^[^\n]*[\n ]+;;
+      s;\n+$;;;
+
+      p;
+    }
+    ' \
+  | while read -r line; do
+    declare -A tag
+    case "$line" in
+      value*) eval "$line";;
+      tag*)   eval "$line";;
+      key*)
+        if [ -z "$key" ]; then
+          eval "$line"
+        else
+          values[$key]="$value"
+          for t in ${(k)tag}; do
+            values[${key}_$t]="$tag[$t]"
+          done
+          eval "$line"
+          if [ -n "$values[$key]" ]; then
+            n=0
+            while [ -n "$values[$key$n]" ]; do n=$(($n + 1)); done
+            key=$key$n
+          fi
+          unset value
+          unset tag
+        fi
+      ;;
+    esac
+  done
+}
+
+view_course() {  #Parameter: Calendarfile
+  id="$1"
+  calendarfile="$_DATA/ical/${id}"
+  cachefile="$_DATA/cache/${id}.cache"
+  unset key
+  if [ "$cachefile" -nt "$calendarfile" ]; then
+    cat "$cachefile"
+  else
+    declare -A values
+    ics_parse "$calendarfile"
+    . ${_EXEC}/templates/view_course.sh |tee "$cachefile"
+  fi
+}
+
+edit_course() {  #Parameter: Calendarfile
+  id="$1"
+  calendarfile="$_DATA/ical/$id"
+  tempfile="$_DATA/temp/$id"
+  [ -f "$tempfile" ] && calendarfile="$tempfile"
+  unset key
+
+  declare -A values
+  ics_parse "$calendarfile"
+  . ${_EXEC}/templates/edit_course.sh
+}
diff --git a/courses/edit_course.sh b/courses/edit_course.sh
new file mode 100755 (executable)
index 0000000..1841e05
--- /dev/null
@@ -0,0 +1,22 @@
+#!/bin/zsh
+
+# Copyright 2014 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+course="${_GET[course]}"
+
+echo -n "Location: ?p=courses&edit=$course\n\n"
diff --git a/courses/edit_course.template.sh b/courses/edit_course.template.sh
new file mode 100755 (executable)
index 0000000..0e1cd73
--- /dev/null
@@ -0,0 +1,176 @@
+# Copyright 2014 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+SUP_FIELDS=(SUMMARY COMMENT)
+[ -z "$values[UID]" ] && values[UID]="$(uuidgenerator)"
+
+dtstart="$values[DTSTART]"
+[ -z "$dtstart" ] && dtstart=$(date +%Y%m%dT%H%M%S)
+
+echo "$dtstart" |case "$dtstart" in
+  *Z)
+    sed -rn 's:^([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})Z$:\1-\2-\3 \4\:\5\:\6 UTC:p'
+    ;;
+  TZID*)
+    sed -rn 's:^TZID=(.+)\:([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})$:TZ="\1" \2-\3-\4 \5\:\6\:\7:p'
+    ;;
+  *)
+    sed -rn 's:^([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})$:\1-\2-\3 \4\:\5\:\6:p'
+    ;;
+esac |read dts_date
+
+dts_year=$(date -d "$dts_date" +%Y)
+dts_month=$(date -d "$dts_date" +%m)
+dts_dom=$(date -d "$dts_date" +%d)
+dts_dow=$(date -d "$dts_date" +%u)
+dts_weekday=$(date -d "$dts_date" +%A)
+dts_hour=$(date -d "$dts_date" +%H)
+dts_min=$(date -d "$dts_date" +%M)
+dts_sec=$(date -d "$dts_date" +%S)
+
+rrule="$values[RRULE]"
+rr_int="$(echo $rrule |sed -rn 's:^.*INTERVAL=([0-9]+)(;.*)?$:\1:p')"
+rr_count="$(echo $rrule |sed -rn 's:^.*COUNT=([0-9]+)(;.*)?$:\1:p')"
+rr_freq="$(echo $rrule |sed -rn 's:^.*FREQ=(YEARLY|MONTHLY|WEEKLY|DAILY)(;.*)?$:\1:p')"
+rr_uyear="$(echo $rrule |sed -rn 's:^.*UNTIL=([0-9]{4})([0-9]{2})([0-9]{2})T[0-9]{6}Z(;.*)?$:\1:p')"
+rr_umonth="$(echo $rrule |sed -rn 's:^.*UNTIL=([0-9]{4})([0-9]{2})([0-9]{2})T[0-9]{6}Z(;.*)?$:\2:p')"
+rr_uday="$(echo $rrule |sed -rn 's:^.*UNTIL=([0-9]{4})([0-9]{2})([0-9]{2})T[0-9]{6}Z(;.*)?$:\3:p')"
+if [ -n "$rr_count" ]; then
+  rr_limit=COUNT
+elif [ -n "$rr_uyear" ]; then
+  rr_limit=UNTIL
+else
+  rr_limit=ETERN
+fi
+
+echo '<div id="'${id}'" class="course">'
+echo '<form action="?action=update_course" method="POST">'
+echo '<input type="hidden" name="course" value="'${id}'"/>'
+echo '  <div class="section basic">'
+echo '    <h2>'$(l10n SUMMARY)'</h2>'
+echo '    <input type="text" class="item SUMMARY" name="SUMMARY" value="'$values[SUMMARY]'" />'
+echo '    <h3>'$(l10n COMMENT)'</h3>'
+for n in COMMENT COMMENT{0..10}; do [ -n "$values[$n]" ] &&\
+  echo '    <textarea class="item COMMENT" name="COMMENT">'$values[$n]'</textarea>'
+done
+
+echo '  </div><div class="section dtstart">'
+echo '    <h3>'$(l10n DTSTART)'</h3>'
+echo '    <select class="DTSYEAR" name="DTSYEAR" onchange="this.form.submit();">'
+for y in {$(($dts_year - 50))..$(($dts_year + 50))}; do
+  echo '    <option value="'$y'" '$([ "$y" -eq "$dts_year" ] && echo 'selected="selected"')'>'$y'</option>'
+done
+echo '    </select>'
+echo '    <select class="DTSMONTH" name="DTSMONTH" onchange="this.form.submit();">'
+for m in {01..12}; do
+  echo '    <option value="'$m'" '$([ "$m" -eq "$dts_month" ] && echo 'selected="selected"')'>'$(date -d ${dts_year}-${m}-1 +%B)'</option>'
+done
+echo '    </select>'
+echo '    <button class="DTS" type="submit" name="DTS" value="update">'$(l10n edit_dtscal)'</button><br/>'
+for wd in mon tue wed thu fri sat sun; do
+  echo -n '<span class="DTSCALHEAD">'$(date -d $wd +%a)'</span>'
+done
+fdom=$(date -d ${dts_year}-${dts_month}-1 +%u)
+while [ "$fdom" -gt 1 ]; do
+  echo -n '<span class="DTSCAL"></span>'
+  fdom=$(($fdom - 1))
+done
+cnt=$(date -d ${dts_year}-${dts_month}-1 +%s)
+while [ "$(date -d @$cnt +%m)" = "$dts_month" ]; do
+  dn=$(date -d @$cnt +%d)
+  echo -n '<input type="radio" class="DTSCAL" name="DTSDAY" value="'$dn'" id="DTSCAL_'$dn'" '$([ $dn = $dts_dom ] && echo checked)'><label for="DTSCAL_'$dn'" class="DTSCAL">'$dn'</label>'
+  cnt=$(($cnt + 86400))
+done
+echo '    <br />'
+echo '    <span class="DTSTIME">'$(l10n time)':</span>'
+echo '    <select class="DTSTIME" name="DTSHOUR">'
+for h in {00..23}; do
+  echo '    <option value="'$h'" '$([ "$h" -eq "$dts_hour" ] && echo 'selected="selected"')'>'$h'</option>'
+done
+echo '    </select>:<select class="DTSTIME" name="DTSMINUTE">'
+for m in 00 05 10 15 20 25 30 35 40 45 50 55; do
+  echo '    <option value="'$m'" '$([ "$m" -eq "$dts_min" ] && echo 'selected="selected"')'>'$m'</option>'
+done
+echo '    </select>'
+
+echo '  </div><div class="section recur">'
+echo '    <h3>'$(l10n RRULE)'</h3>'
+echo '    <span class="item">'
+echo '    '$(l10n t_every)''
+echo '    <select class="RRULE INTERVAL" name="RRULE_INTERVAL">'
+for i in {1..365}; do
+  echo '    <option value="'$i'" '$([ "$i" = "$rr_int" ] && echo 'selected="selected"')'>'$i'</option>'
+done
+echo '    </select>'
+echo '    <select class="RRULE FREQ" name="RRULE_FREQ">'
+for f in DAILY WEEKLY MONTHLY YEARLY; do
+  echo '    <option value="'$f'" '$([ "$f" = "$rr_freq" ] && echo 'selected="selected"')'>'$(l10n $f)'</option>'
+done
+echo '    </select></span>'
+echo '    <span class="item">'
+echo '    <input type="radio" name="RRULE_LIMIT" value="ETERN" '$([ "$rr_limit" = ETERN ] && echo 'checked="checked"')'>'$(l10n t_eternal)'</input><br/>'
+echo '    </span><span class="item">'
+echo '    <input type="radio" name="RRULE_LIMIT" value="COUNT" '$([ "$rr_limit" = COUNT ] && echo 'checked="checked"')'></input>'
+echo '    <select class="RRULE COUNT" name="RRULE_COUNT">'
+for t in {1..365}; do
+  echo '    <option value="'$t'" '$([ "$t" = "$rr_count" ] && echo 'selected="selected"')'>'$t'</option>'
+done
+echo '    </select>'$(l10n t_times)'</span>'
+echo '    <span class="item">'
+echo '    <input type="radio" name="RRULE_LIMIT" value="UNTIL" '$([ "$rr_limit" = UNTIL ] && echo 'checked="checked"')'>'$(l10n t_until)'</input>'
+echo '    <select class="RRULE UYEAR" name="RRULE_UYEAR">'
+for uy in {${dts_year}..$(($dts_year + 50))}; do
+  echo '    <option value="'$uy'" '$([ "$uy" = "$rr_uyear" ] && echo 'selected="selected"')'>'$uy'</option>'
+done
+echo '    </select>'
+echo '    <select class="RRULE UMONTH" name="RRULE_UMONTH">'
+for um in {01..12}; do
+  echo '    <option value="'$um'" '$([ "$um" = "$rr_umonth" ] && echo 'selected="selected"')'>'$(date -d 2000-${um}-1 +%B)'</option>'
+done
+echo '    </select>'
+echo '    <select class="RRULE UDAY" name="RRULE_UDAY">'
+for ud in {01..31}; do
+  echo '    <option value="'$ud'" '$([ "$ud" = "$rr_uday" ] && echo 'selected="selected"')'>'$ud'</option>'
+done
+echo '    </select></span>'
+
+echo '  </div><div class="section select_attendance">'
+echo '    <h3>'$(l10n course_attendance)'</h3>'
+listcards |while read card; do
+  cfile="$_DATA/vcard/$card"
+  aname=$(sed -rn 's:^N(;[^"\:]+|;"[^"]+")*\:([^;]*)(\;[^;]*)(\;[^;]*)?(\;[^;]*)?(\;[^;]*)?$:\5 \3 \4 \2 \6:p' "$cfile" \
+          |sed -r 's:,: :;s:\;: :g;s: +: :g' \
+          |tr -d '\r'
+         )
+  selected=$(sed -rn "/^$id\t$card\$/{s;^.*\$;checked=\"checked\";p;q}" "${_DATA}/mappings/attendance")
+  echo '    <label><input type="checkbox" class="item" name="attendance" value="'$card'" '$selected'>'$aname'</label>'
+done
+
+echo '  </div><div class="control">'
+echo '    <select class="item" name="newfield">'
+echo '      <option value="" disabled="disabled" selected="selected">'$(l10n edit_addfieldtext)'</option>'
+            for f in $SUP_FIELDS; do echo "<option value=\"$f\">$(l10n $f)</option>"; done
+echo '    </select'
+echo '    ><button class="item" type="submit" name="action" value="addfield">'$(l10n edit_addfield)'</button>'
+echo '    <button class="item" type="submit" name="action" value="update">'$(l10n edit_update)'</button>'
+echo '    <button class="item" type="submit" name="action" value="delete">'$(l10n edit_delete)'</button>'
+echo '    <button class="item" type="submit" name="action" value="cancel">'$(l10n edit_cancel)'</button>'
+echo '  </div>'
+echo '<input type="hidden" name="UID" value="'$values["UID"]'"/>'
+echo '</form>'
+echo '</div>'
+
diff --git a/courses/export_ical.sh b/courses/export_ical.sh
new file mode 100755 (executable)
index 0000000..e595cb6
--- /dev/null
@@ -0,0 +1,22 @@
+#!/bin/zsh
+
+# Copyright 2014,2015 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+echo -n "Content-Type: text/calendar;charset=utf-8\n\n"
+course="${_GET[course]}"
+cat "$_DATA/ical/$course"
diff --git a/courses/generate_courselist.sh b/courses/generate_courselist.sh
new file mode 100755 (executable)
index 0000000..d777536
--- /dev/null
@@ -0,0 +1,109 @@
+#!/bin/zsh
+
+# Copyright 2014, 2016, 2017 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+pdflatex="$(where pdflatex |head -n1 || echo false)"
+course="${_GET[course]}"
+fromdate="${_GET[fromdate]}"
+fromdate="$(date -d "$fromdate" +%s)" 2>/dev/null
+[ -z "$fromdate" ] && fromdate=$(date +%s)
+
+. ${_EXEC}/pages/courses.sh
+. ${_EXEC}/pages/cards.sh
+
+tex_clean() { #in dire need for improvement
+  printf %s "$*" |tr -d '{&}\\"'
+}
+
+
+list_attendee() {  #Parameter: Cardfile
+  id="$1"
+  cardfile="$_DATA/vcard/${id}"
+
+  declare -A values
+
+  if [ -r "$cardfile" ]; then
+    vcf_parse "$cardfile"
+
+    n=$(printf %s "$values[N]" \
+        | sed -rn 's:^([^;]*)(;[^;]*)(;[^;]*)?(;[^;]*)?(;[^;]*)?$:\4 \2 \3 \1 \5:gp' \
+        | sed -r 's:,: :;s:;: :g;s: +: :g;s:^ $::;'
+       )
+    fullname="${n:-${values[FN]:-${values[NICKNAME]}}}"
+
+    tel=''
+    for n in TEL TEL{0..10}; do if (echo "$values[$n]" |grep -Eq '[0-9]'); then
+      [ -n "$tel" ] && tel="$tel\\newline $(tex_clean "$values[$n]")" || tel="$(tex_clean "$values[$n]")"
+    fi; done
+
+    note=''
+    for n in NOTE NOTE{0..10}; do if [ -n "$values[$n]" ]; then
+      [ -n "$note" ] && note="$note\\newline $(tex_clean "$values[$n]")" || note="$(tex_clean "$values[$n]")"
+    fi; done
+    printf '%s & %s & %s & %s\n' \
+           "$(tex_clean $fullname)" "$(tex_clean $values[BDAY])" "$tel" "$note" \
+    | sed -r ':X;N;$!bX; s;\n;\\newline ;g'
+  fi
+}
+
+get_dates() {  #Parameter: Calendarfile
+  calendarfile="$_DATA/ical/$course"
+
+  declare -A values
+  ics_parse "$calendarfile"
+
+  dtstart="$values[DTSTART]"
+  [ -z "$dtstart" ] && dtstart=$(date +%Y%m%dT%H%M%S)
+  echo "$dtstart" |case "$dtstart" in
+    *Z)    sed -rn 's:^([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})Z$:\1-\2-\3 \4\:\5\:\6 UTC:p';;
+    TZID*) sed -rn 's:^TZID=(.+)\:([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})$:TZ="\1" \2-\3-\4 \5\:\6\:\7:p';;
+    *)     sed -rn 's:^([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})$:\1-\2-\3 \4\:\5\:\6:p';;
+  esac |read dts_date
+  rrule="$values[RRULE]"
+  rr_int="$(echo $rrule |sed -rn 's:^.*INTERVAL=([0-9]+)(;.*)?$:\1:p')"
+  rr_freq="$(echo $rrule |sed -rn 's:^.*FREQ=(YEARLY|MONTHLY|WEEKLY|DAILY)(;.*)?$:\1:p')"
+  case "$rr_freq" in
+    YEARLY) rec="$rr_int year";;
+    MONTHLY) rec="$rr_int month";;
+    DAILY) rec="$rr_int day";;
+    *) rec="$rr_int week";;
+  esac
+
+  next_date="$dts_date"
+  n=10
+  while [ $n -gt 0 ]; do
+    if [ "$(date -d "$next_date" +%s)" -gt "$(date +%s)" ]; then
+      dtlist="$dtlist & $(date -d "$next_date" +"%d. %b.")"
+      n=$(($n - 1))
+    fi
+    next_date="$(date -d "$next_date + $rec" +%Y-%m-%d)"
+  done
+
+  echo "$dtlist"
+}
+
+if [ -r "${_DATA}/ical/${course}" ]; then
+  . ${_EXEC}/templates/course_print.sh >"${_DATA}/temp/courselist_${course}.tex"
+  [ -e "${_DATA}/temp/courselist_${course}.pdf" ] && rm "${_DATA}/temp/courselist_${course}.pdf"
+  "$pdflatex" -halt-on-error -output-directory "${_DATA}/temp/" "${_DATA}/temp/courselist_${course}.tex" |debug >/dev/null
+  "$pdflatex" -halt-on-error -output-directory "${_DATA}/temp/" "${_DATA}/temp/courselist_${course}.tex" |debug >/dev/null
+fi
+if [ -r "${_DATA}/temp/courselist_${course}.pdf" ]; then
+  echo 'Content-Type: application/x-pdf\n'
+  cat "${_DATA}/temp/courselist_${course}.pdf"
+fi
diff --git a/courses/index.cgi b/courses/index.cgi
new file mode 100755 (executable)
index 0000000..446cc9f
--- /dev/null
@@ -0,0 +1,33 @@
+#!/bin/sh
+
+if [ "$_PATH" = "/courses/courses.css" ]; then
+  . $_EXEC/cgilite/file.sh
+  FILE $_EXEC/courses/courses.css
+  return 0
+fi
+
+. $_EXEC/pdiread.sh
+. $_EXEC/courses/l10n.sh
+. $_EXEC/courses/widgets.sh
+. $_EXEC/courses/list.sh
+
+unescape() { [ $# = 0 ] && sed -E 's;\\(.);\1;g' || printf %s "$*" |sed -E 's;\\(.);\1;g'; }
+upcase=' y;abcdefghijklmnopqrstuvwxyzäöüé;ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÜÉ;; '
+
+order="$(GET o |grep -m1 -xE 'DOW|TOD')"
+edit="$(GET e |PATH)"
+
+[ "$order" ] || order=DOW
+edit="${edit##*/}"
+
+{ w_sort_courses
+  printf '
+  [form .newcourses action="/cards/new_course.sh" method="POST"
+    [button type="submit" %s]
+  ]' "$(l10n newcourse)"
+
+  [ "$edit" ] && edit_course "$edit"
+  printf '[div .courselist\n'
+    list_courses
+  printf ']'
+} | yield_page courses /courses/courses.css
diff --git a/courses/l10n.sh b/courses/l10n.sh
new file mode 100755 (executable)
index 0000000..711916e
--- /dev/null
@@ -0,0 +1,65 @@
+# Copyright 2014, 2016, 2019 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+l10n(){
+  local word
+  [ $# -eq 0 ] && read -r word || word="$1"
+
+  case $word in
+    SUMMARY) printf "Bezeichnung";;
+    COMMENT) printf "Kommentar";;
+    DTSTART) printf "Beginn";;
+    DURATION) printf "Dauer";;
+    RRULE) printf "Regelmäßigkeit";;
+    
+    DAILY) printf "Tage";;
+    WEEKLY) printf "Wochen";;
+    MONTHLY) printf "Monate";;
+    YEARLY) printf "Jahre";;
+    
+    sDAILY) printf "Täglich";;
+    sWEEKLY) printf "Wöchentlich";;
+    sMONTHLY) printf "Monatlich";;
+    sYEARLY) printf "Jährlich";;
+    
+    newcourse) printf "Neuen Eintrag anlegen";;
+    time) printf "Uhrzeit";;
+    edit_update) printf "Daten übernehmen";;
+    edit_cancel) printf "Abbrechen";;
+    edit_delete) printf "Eintrag löschen";;
+    edit_addfieldtext) printf "Neues Feld";;
+    edit_addfield) printf "+";;
+    edit_dtscal) printf "&#x2713;";;
+    edit) printf "Bearbeiten";;
+    ics_export) printf "ICal exportieren";;
+    courselist) printf "Kursliste (PDF)";;
+    
+    course_attendance) printf "Teilnahme";;
+    course_mail) printf "Mail an Teilnehmende";;
+    
+    sort_order) printf "Sortierung";;
+    order_DOW) printf "Wochentag";;
+    order_TOD) printf "Uhrzeit";;
+    order_apply) printf "Sortieren";;
+    
+    t_every) printf "Alle";;
+    t_eternal) printf "ewig";;
+    t_times) printf "mal";;
+    t_until) printf "Bis";;
+    t_oclock) printf "Uhr";;
+  esac
+}
diff --git a/courses/list.sh b/courses/list.sh
new file mode 100755 (executable)
index 0000000..abf3bf9
--- /dev/null
@@ -0,0 +1,80 @@
+#!/bin/sh
+
+. "${_EXEC}"/pdiread.sh
+
+edit_card(){
+  local coursefile="$_DATA/courses/$1" 
+  local tempfile course
+
+  . $_EXEC/session_lock.sh
+
+  if ! tempfile="$(CHECK_SLOCK "$coursefile")"; then
+    printf '[div .message %s]' "$(l10n "This course is not set up for editing within this session.")"
+  else
+    course="$(pdi_load "$tempfile")"
+    cat <<-EOF
+       [form .course #${coursefile##*/} action="/cards/update_course.sh" method="POST"
+         [input type="hidden" name="tid" value="$(transid ${tempfile})"]
+       ]
+       EOF
+  fi
+}
+
+print_course(){
+  local coursefile="$1"
+  local course="$(pdi_load "$coursefile")"
+  cat <<-EOF
+    [div .course #${coursefile##*/}
+      [div .section .basic &shy;$(
+        cal_item "$course" SUMMARY DTSTART RRULE
+      )]
+      [div .section .COMMENT   &shy;$(cal_item "$course" COMMENT)]
+      [div .section .attendance [h3 $(l10n course_attendance) ] [ul &shy;
+        $(grep -F "    ${cardfile##*/}" "$_DATA/mappings/attendance" |while read each discard; do
+          printf '[li [a .item .attendance href="/courses#%s" &shy;%s]]' \
+                 "$each" \
+                 "$(pdi_value "$(pdi_load "$_DATA/ical/$each")" SUMMARY |unescape |HTML)"
+        done)]
+      ]
+      [div .control
+        [a .item href="/courses/edit_course.sh?course=${coursefile##*/}" $(l10n edit)]
+        [a .item href="/courses/export_pdf.sh?course=${coursefile##*/}" target="blank" $(l10n courselist)]
+        [a .item href="/courses/export_ical.sh?course=${coursefile##*/}" $(l10n ics_export)]
+        [a .item href="mailto:zack@vuesch.org?bcc=$(course_mail "${coursefile##*/}")" $(l10n course_mail)]
+      ]
+    ]
+       EOF
+}
+
+print_courses(){
+  local calfile cachefile date size name ldate=0 lsize lname
+
+  while read calfile; do
+    cachefile="${_DATA}/cache/${calfile##*/}.cache"
+    if [ -s "$cachefile" -a "$cachefile" -nt "$calfile" ]; then
+      cat "$cachefile"
+    else
+      print_course "$calfile" |tee "$cachefile"
+    fi
+  done
+}
+
+order_courses() {
+  local calfile course
+
+  while read calfile; do
+    icstime="$(pdi_value "$(pdi_load "$calfile")" DTSTART |cal_date)"
+    case $order in
+      DOW) printf '%s  %s\n' "$(date -d "$icstime" "+%u %H:%M:%S")" "$calfile";;
+      TOD) printf '%s  %s\n' "$(date -d "$icstime" "+%H:%M:%S")" "$calfile";;
+    esac
+  done \
+  | sort \
+  | sed -E 's;^.*\t;;g'
+}
+
+list_courses(){
+  printf '%s\n' ${_DATA}/courses/*.ics \
+  | order_courses \
+  | print_courses
+}
diff --git a/courses/new_course.sh b/courses/new_course.sh
new file mode 100755 (executable)
index 0000000..4f97d4a
--- /dev/null
@@ -0,0 +1,46 @@
+#!/bin/zsh
+
+# Copyright 2014 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+cgi_post
+
+uid=$(uuidgenerator)
+course="${uid}.ics"
+
+tzid=$(cat /etc/timezone)
+tstamp=$(TZ="$tzid" date +%Y%m%dT%H%M%S)
+
+tempfile="$_DATA/temp/$course"
+
+cat >"$tempfile" <<EOF
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:Berlin RAW Confetti
+BEGIN:VEVENT
+UID:$uid
+DTSTAMP:TZID=${tzid}:${tstamp}
+DTSTART:
+DURATION:
+RRULE:
+SUMMARY:
+COMMENT:
+END:VEVENT
+END:VCARD
+EOF
+
+echo -n "Location: ?p=courses&edit=$course\n\n"
diff --git a/courses/update_course.sh b/courses/update_course.sh
new file mode 100755 (executable)
index 0000000..a5ba8ce
--- /dev/null
@@ -0,0 +1,130 @@
+#!/bin/zsh
+
+# Copyright 2014, 2015 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+cgi_post
+
+course="${_POST[course]}"
+tempfile="temp/$course"
+coursefile="ical/$course"
+attfile="$_DATA/mappings/attendance"
+
+# DURATION:
+uid="${_POST[UID]}"
+
+tzid=$(cat /etc/timezone)
+tstamp=$(TZ="$tzid" date +%Y%m%dT%H%M%S)
+
+dts_year="${_POST[DTSYEAR]}"
+dts_month="${_POST[DTSMONTH]}"
+dts_day="${_POST[DTSDAY]}"
+[ -n "${_POST[DTSDAY0]}" ] && dts_day="${_POST[DTSDAY0]}"
+dts_hour="${_POST[DTSHOUR]}"
+dts_minute="${_POST[DTSMINUTE]}"
+
+[ -z $dts_year ] && dts_year=$(date +%Y)
+[ -z $dts_month ] && dts_month=$(date +%m)
+[ -z $dts_day ] && dts_day=$(date +%d)
+date -d ${dts_year}-${dts_month}-${dts_day} >/dev/null 2>/dev/null || dts_day="01"
+[ -z $dts_hour ] && dts_hour=$(date +%H)
+[ -z $dts_minute ] && dts_minute=$(date +%M)
+
+dtstart="TZID=${tzid}:${dts_year}${dts_month}${dts_day}T${dts_hour}${dts_minute}00"
+
+rr_int="${_POST[RRULE_INTERVAL]}"
+rr_freq="${_POST[RRULE_FREQ]}"
+rr_limit="${_POST[RRULE_LIMIT]}"
+case "$rr_limit" in
+  ETERN)
+    rrule="FREQ=$rr_freq;INTERVAL=$rr_int"
+    ;;
+  COUNT)
+    t="${_POST[RRULE_COUNT]}"
+    rrule="FREQ=$rr_freq;INTERVAL=$rr_int;COUNT=$t"
+    ;;
+  UNTIL)
+    uy="${_POST[RRULE_UYEAR]}"
+    um="${_POST[RRULE_UMONTH]}"
+    ud="${_POST[RRULE_UDAY]}"
+    rrule="FREQ=$rr_freq;INTERVAL=$rr_int;UNTIL=${uy}${um}${ud}T000000Z"
+    ;;
+esac
+
+echo "BEGIN:VCALENDAR\r" >"$tempfile"
+echo "VERSION:2.0\r" >>"$tempfile"
+echo "PRODID:Berlin RAW Confetti\r" >>"$tempfile"
+echo "BEGIN:VEVENT\r" >>"$tempfile"
+echo "UID:$uid\r" >>"$tempfile"
+echo "DTSTAMP:TZID=${tzid}:${tstamp}\r" >>"$tempfile"
+echo "DTSTART:${dtstart}\r" >>"$tempfile"
+echo "RRULE:${rrule}\r" >>"$tempfile"
+for field in SUMMARY COMMENT; do
+  value="${_POST[$field]}"
+  n=0
+  while [ -n "$value" ]; do
+    value="$(echo "$value" |sed -r ':a;N;$!ba;s:\n:\\\\n:g;s:\r:\\\\r:g')"
+    echo "${field}:${value}\r"
+    value="${_POST[$field$n]}"
+    n=$(($n + 1))
+  done
+done >>"$tempfile"
+
+case "${_POST[action]}" in
+  addfield)
+    echo "${_POST[newfield]}:\r" >>"$tempfile"
+    echo "END:VEVENT\r" >>"$tempfile"
+    echo "END:VCALENDAR\r" >>"$tempfile"
+    echo -n "Location: ?p=courses&edit=$course\n\n"
+    ;;
+  update)
+    attendance=()
+    for att in attendance attendance{0..100}; do
+      [ -n "${_POST[$att]}" ] && attendance+=("${_POST[$att]}")
+    done
+    sed -rn 's:^'$course'\t(.+)$:\1:p' "$attfile" |while read card; do
+      touch "$_DATA/vcard/$card"
+    done
+    sed -i -r '/^'$course'\t(.+)$/d' "$attfile"
+    for each in $attendance; do
+      echo "$course\t$each"
+    done >>"$attfile"
+    sed -rn 's:^'$course'\t(.+)$:\1:p' "$attfile" |while read card; do
+      touch "$_DATA/vcard/$card"
+    done
+
+    echo "END:VEVENT\r" >>"$tempfile"
+    echo "END:VCALENDAR\r" >>"$tempfile"
+    mv "$tempfile" "$coursefile"
+    echo -n "Location: ?p=courses#$course\n\n"
+    ;;
+  cancel)
+    rm "$tempfile"
+    [ -f "$coursefile" ] \
+      && echo -n "Location: ?p=courses#$course\n\n" \
+      || echo -n "Location: ?p=courses\n\n"
+    ;;
+  delete)
+    rm "$tempfile" "$coursefile"
+    echo -n "Location: ?p=courses\n\n"
+    ;;
+  *)
+    echo "END:VEVENT\r" >>"$tempfile"
+    echo "END:VCALENDAR\r" >>"$tempfile"
+    echo -n "Location: ?p=courses&edit=$course\n\n"
+    ;;
+esac
diff --git a/courses/view_course.sh b/courses/view_course.sh
new file mode 100755 (executable)
index 0000000..23088f1
--- /dev/null
@@ -0,0 +1,102 @@
+# Copyright 2014 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+dtstart="$values[DTSTART]"
+[ -z "$dtstart" ] && dtstart=$(date +%Y%m%dT%H%M%S)
+
+echo "$dtstart" |case "$dtstart" in
+  *Z)
+    sed -rn 's:^([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})Z$:\1-\2-\3 \4\:\5\:\6 UTC:p'
+    ;;
+  TZID*)
+    sed -rn 's:^TZID=(.+)\:([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})$:TZ="\1" \2-\3-\4 \5\:\6\:\7:p'
+    ;;
+  *)
+    sed -rn 's:^([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})$:\1-\2-\3 \4\:\5\:\6:p'
+    ;;
+esac |read dts_date
+
+dts_year=$(date -d "$dts_date" +%Y)
+dts_month=$(date -d "$dts_date" +%m)
+dts_dom=$(date -d "$dts_date" +%d)
+dts_dow=$(date -d "$dts_date" +%u)
+dts_weekday=$(date -d "$dts_date" +%A)
+dts_hour=$(date -d "$dts_date" +%H)
+dts_min=$(date -d "$dts_date" +%M)
+dts_sec=$(date -d "$dts_date" +%S)
+
+rrule="$values[RRULE]"
+rr_int="$(echo $rrule |sed -rn 's:^.*INTERVAL=([0-9]+)(;.*)?$:\1:p')"
+rr_count="$(echo $rrule |sed -rn 's:^.*COUNT=([0-9]+)(;.*)?$:\1:p')"
+rr_freq="$(echo $rrule |sed -rn 's:^.*FREQ=(YEARLY|MONTHLY|WEEKLY|DAILY)(;.*)?$:\1:p')"
+rr_uyear="$(echo $rrule |sed -rn 's:^.*UNTIL=([0-9]{4})([0-9]{2})([0-9]{2})T[0-9]{6}Z(;.*)?$:\1:p')"
+rr_umonth="$(echo $rrule |sed -rn 's:^.*UNTIL=([0-9]{4})([0-9]{2})([0-9]{2})T[0-9]{6}Z(;.*)?$:\2:p')"
+rr_uday="$(echo $rrule |sed -rn 's:^.*UNTIL=([0-9]{4})([0-9]{2})([0-9]{2})T[0-9]{6}Z(;.*)?$:\3:p')"
+rr_udate="$(echo $rrule |sed -rn 's:^.*UNTIL=([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})Z(;.*)?$:\1-\2-\3 \4\:\5\:\6:p')"
+if [ -n "$rr_count" ]; then
+  rr_limit=COUNT
+elif [ -n "$rr_uyear" ]; then
+  rr_limit=UNTIL
+else
+  rr_limit=ETERN
+fi
+
+coursemail=""
+
+echo '<div id="'${id}'" class="course">'
+echo '<form action="?action=update_course" method="POST">'
+echo '<input type="hidden" name="course" value="'${id}'"/>'
+echo '  <div class="section basic">'
+echo '    <h2>'$values[SUMMARY]'</h2>'
+echo '    <span class="text">'$(date -d "$dts_date" "+%A %B %d, %Y - %H:%M") $(l10n t_oclock)'</span>'
+[ "$rr_int" -eq 1 ] \
+&& echo '<span class="text">'$(l10n s$rr_freq)'</span>' \
+|| echo '<span class="text">'$(l10n t_every) ${rr_int} $(l10n $rr_freq)'</span>'
+case "$rr_limit" in
+  COUNT)
+    m1=$(echo $rr_freq |sed -r "s:DAILY:day:g;s:WEEKLY:week:g;s:MONTHLY:month:g;s:YEARLY:year:g;")
+    m2=$(($rr_int * $rr_count))
+    echo '<span class="text">'$(l10n t_until) $(date -d "$dts_date + $m2 $m1" "+%A %B %d, %Y - %H:%M")'</span>'
+    ;;
+  UNTIL)
+    echo '<span class="text">'$(l10n t_until) $(date -d "$rr_udate" "+%A %B %d, %Y - %H:%M")'</span>'
+    ;;
+esac
+echo '  </div>'
+
+echo '  <div class="section COMMENT">'
+echo '    <h3>'$(l10n COMMENT)'</h3>'
+for n in COMMENT COMMENT{0..10}; do [ -n "$values[$n]" ] &&\
+  echo '    <p class="item COMMENT">'$(echo "$values[$n]" |sed -r "s:$:<br/>:g" )'</p>'
+done
+echo '  </div>'
+
+echo '  <div class="section attendance">'
+echo '    <h3>'$(l10n course_attendance)'</h3>'
+list_attendance "$id" |sort -k 2 |sed -r 's:^([^ ]+) (.*)$:<a class="attendance" href="?p=cards#\1">\2</a>:'
+echo '  </div>'
+
+echo '  <div class="control">'
+echo '    <a class="item" href="?action=edit_course&course='${id}'">'$(l10n edit)'</a>'
+echo '    <a class="item" href="?action=generate_courselist&course='${id}'" target="_blank">'$(l10n courselist)'</a>'
+echo '    <a class="item" href="?action=export_ical&course='${id}'">'$(l10n ics_export)'</a>'
+echo '    <a class="item" href="mailto:zack@vuesch.org?bcc='$(course_mail "$id")'">'$(l10n course_mail)'</a>'
+echo '  </div>'
+echo '<input type="hidden" name="UID" value="'$values["UID"]'"/>'
+echo '</form>'
+echo '</div>'
+
diff --git a/courses/widgets.sh b/courses/widgets.sh
new file mode 100755 (executable)
index 0000000..41fc686
--- /dev/null
@@ -0,0 +1,180 @@
+# Copyright 2014, 2019 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+check(){
+  [ "$1" = "$2" ] && printf 'checked="checked"'
+}
+
+edit="$(GET e)"
+order="$(GET o |grep -m1 -xE 'DOW|TOD')"
+
+w_sort_courses(){
+  cat <<-EOF
+       [form .sort .search action="?" method="GET"
+         [span .label $(l10n sort_order):]
+         [radio "order" "DOW" $(check $order DOW) $(l10n order_DOW)]
+         [radio "order" "TOD" $(check $order TOD) $(l10n order_TOD)]
+         [submit "" "" $(l10n order_apply)]
+       ]
+       EOF
+}
+
+cal_date(){
+  { [ $# -eq 0 ] && cat || printf %s "$*"; } |sed -rnE '
+    2q
+    s/^([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})Z$/\1-\2-\3 \4:\5:\6 UTC/p;t
+    s/^TZID=(.+)\:([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})$/TZ="\1" \2-\3-\4 \5:\6:\7/p;t
+    s/^([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})$/\1-\2-\3 \4:\5:\6/p;t
+  '
+}
+
+cal_item(){
+  local course="$1"
+  local item cnt c
+  shift 1
+
+  for item in $@; do
+    cnt="$(pdi_count "$course" "$item")"
+
+    case $item in
+      SUMMARY)
+        printf '[h2 &shy%s]' "$(pdi_value "$course" SUMMARY)"
+        ;;
+      DTSTART)
+        printf '[span .text .DTSTART %s %s ]' \
+               "$(date -d "$(pdi_value "$course" DTSTART |cal_date)" '+%A %B %d, %Y - %H:%M')" \
+               "$(l10n t_oclock)"
+        ;;
+      RRULE)
+        dts_date="$(pdi_value "$course" DTSTART |cal_date)"
+        rrule=" $(pdi_value "$course" RRULE)"
+        rr_int="${rrule##*INTERVAL=}"; rr_int="${rr_int%%;*}"
+        rr_count="${rrule##*COUNT=}"; rr_count="${rr_count%%;*}"
+        rr_freq="${rrule##*FREQ=}"; rr_freq="${rr_freq%%;*}"
+        rr_until="${rrule##*UNTIL=}"; rr_until="${rr_until%%;*}"
+        rr_until="$(cal_date "${rr_until}")"
+
+        [ "$rr_int" -eq 1 ] \
+        && printf '[span .text .RRULE %s]' "$(l10n "s$rr_freq")" \
+        || printf '[span .text .RRULE %s %s %s]' "$(l10n t_every)" "${rr_int}" "$(l10n $rr_freq)"
+        case "$rrule $rr_freq" in
+          *COUNT*DAILY*)
+            printf '[span .text %s %s]' "$(l10n t_until)" "$(date -d "$dts_date + $((rr_int * rr_count)) day" "+%A %B %d, %Y - %H:%M")"
+            ;;
+          *COUNT*WEEKLY*)
+            printf '[span .text %s %s]' "$(l10n t_until)" "$(date -d "$dts_date + $((rr_int * rr_count)) week" "+%A %B %d, %Y - %H:%M")"
+            ;;
+          *COUNT*MONTHLY*)
+            printf '[span .text %s %s]' "$(l10n t_until)" "$(date -d "$dts_date + $((rr_int * rr_count)) month" "+%A %B %d, %Y - %H:%M")"
+            ;;
+          *COUNT*YEARLY*)
+            printf '[span .text %s %s]' "$(l10n t_until)" "$(date -d "$dts_date + $((rr_int * rr_count)) year" "+%A %B %d, %Y - %H:%M")"
+            ;;
+          *UNTIL*)
+            printf '[span .text %s %s]' "$(l10n t_until)" "$(date -d "$rr_until" "+%A %B %d, %Y - %H:%M")"
+            ;;
+        esac
+        ;;
+      attendance);;
+      COMMENT)[ $cnt -gt 0 ] && printf '[h3 %s]' "$(l10n "$item")"
+        seq 1 $cnt |while read c; do
+          printf '[p .item .%s &shy;%s]' "$item" \
+                 "$(pdi_value "$course" "$item" $c |unescape |HTML)"
+        done
+        ;;
+      *)[ $cnt -gt 0 ] && printf '[h3 %s]' "$(l10n "$item")"
+        seq 1 $cnt |while read c; do
+          printf '[span .item .%s &shy;%s]' "$item" \
+                 "$(pdi_value "$course" "$item" $c |unescape |HTML)"
+        done
+        ;;
+    esac
+  done
+}
+
+edit_item(){
+  local course="$1"
+  local item cnt c
+  shift 1
+
+  for item in $@; do
+    cnt="$(pdi_count "$course" "$item")"
+    [ $cnt -lt 1 ] && cnt=1
+
+    case $item in
+      DTSTART)
+        dtstart="$(pdi_value "$course" DTSTART |cal_date)"
+        ystart="${dtstart%%-*}"; ystart="${ystart##* }"
+        mstart="${dtstart#*-}"; mstart="${mstart%%-*}"
+        dstart="${dtstart##*-}"; dstart="${dstart%% *}"
+        hhstart="${dtstart##* }"; hhstart="${hhstart%%:*}"
+        mmstart="${dtstart##*:}";
+
+        printf '[div .section .DTSTART][h3 %s]' "$(l10n DTSTART)"
+        printf '[select .DTSYEAR name="DTSYEAR" onchange="this.form.submit();"'
+        seq $((ystart - 50)) $((ystart + 50)) |while read y; do
+          printf '[option value="%i" %s %i]' $y "$([ $y -eq $ystart ] && printf selected)" $y
+        done
+        printf ']'
+        printf '[select .DTSMONTH name="DTSMONTH" onchange="this.form.submit();"'
+        seq 1 12 |while read m; do
+          printf '[option value="%i" %s %s]' $m "$([ $m -eq $mstart ] && printf selected)" "$(date -d "2000-${m}-01" +%B)"
+        done
+        printf '][submit "DTS" "update" .DTS %s]' "$(l10n edit_dtscal)"
+        printf 'mon tue wed thu fri sat sun' |xargs -n1 date -d '{}' +"[span .DTSCALHEAD %a]"
+        seq 2 "$(date -d ${ystart}-${mstart}-1 +%u)" |xargs -n1 printf '[span .DTSCAL]'
+        cnt=$(date -d ${dts_year}-${dts_month}-1 +%s)
+        dn=1; while [ "$(date ${ystart}-${mstart}-${dn} +%m)" -eq "$mstart" ]; do
+          printf '[input .DTSCAL type="radio" name="DTSDAY" #DTSCAL_%i value="%i" %s][label .DTSCAL for="DTSCAL_%i" %i]' \
+                 $dn $n "$([ $dn -eq "$dstart" ] && printf checked)" $dn $dn
+          dn=$((dn + 1))
+        done 2>/dev/null
+        printf '[span .DTSTIME %s:][select .DTSTIME name="DTSHOUR"' "$(l10n time)"
+        seq 0 23 |while read h; do
+          printf '[option value="%i" %s %i]' $h "$([ $h -eq $hhstart ] && printf checked)" $h
+        done
+        printf ']:[select .DTSTIME name="DTSMINUTE"'
+        seq 0 5 55 |while read m; do
+          printf '[option value="%i" %s %i]' $m "$([ $m -eq $mmstart ] && printf checked)" $m
+        done
+        printf ']'
+        ;;
+      RRULE)
+        ;;
+      COMMENT)
+        printf '[h3 %s]' "$(l10n "$item")"
+        seq 1 $cnt |while read c; do
+          printf '[checkbox "%s_delete_%i" "true" .delete #%s_delete_%i][label for="%s_delete_%i" %s]' \
+            "$item" $c "$item" $c "$item" $c "$(l10n delete)"
+          printf '<textarea class="item %s" name="%s">%s</textarea>' \
+            "$item" "$item" "$(pdi_value "$course" "$item" $c |unescape |HTML)"
+        done
+        printf '[button type="submit" name="action" value="addfield %s" %s ]' "$item" "$(l10n edit_addfield)"
+        ;;
+      attendance);;
+      SUMMARY|*)printf '[h3 %s]' "$(l10n "$item")"
+        seq 1 $cnt |while read c; do
+          printf '[checkbox "%s_delete_%i" "true" .delete #%s_delete_%i][label for="%s_delete_%i" %s]' \
+            "$item" $c "$item" $c "$item" $c "$(l10n delete)"
+          printf '[input .item .%s name="%s" value="%s" placeholder="%s"]' \
+            "$item" "$item" "$(pdi_value "$course" "$item" $c |unescape |HTML)" "$(l10n "$item")"
+        done
+        printf '[button type="submit" name="action" value="addfield %s" %s ]' "$item" "$(l10n edit_addfield)"
+        ;;
+    esac
+  done
+}
diff --git a/email/email.css b/email/email.css
new file mode 100644 (file)
index 0000000..e935189
--- /dev/null
@@ -0,0 +1,157 @@
+/*
+# Copyright 2014 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+*/
+
+.filter {
+  display: block;
+  width: 128ex;
+  border-width: 1px;
+  border-style: solid;
+  border-radius: 4px 4px 0 0;
+  margin: .5em auto .25em auto;
+  padding: .25em 1ex .25em 1ex;
+  background: #EFF;
+}
+
+.filter > .label {
+  display: block;
+  font-weight: bold;
+  font-size: 1.2em;
+  border-style: none none solid none;
+  border-width: 1px;
+  margin: 0em 0ex .5em 0ex;
+  background: #EEF;
+}
+
+.filter .search {
+  margin: 0em 2ex .5em 2ex;
+}
+.filter .search .label {
+  display: inline-block;
+  width: 12ex;
+  font-weight: bold;
+  margin-top: .5em;
+}
+.filter .search input[type="radio"] {
+  margin-top: .5em;
+  margin-left: 1ex;
+}
+.filter .search button {
+  margin-top: .5em;
+}
+
+.filter .search input[type="text"] {
+  width: 124ex;
+}
+
+.newcard {
+  display: block;
+  width: 126ex;
+  border-width: 1px;
+  border-style: solid;
+  border-radius: 0 0 4px 4px;
+  margin: .25em auto 1em auto;
+  padding: .25em 2ex .25em 2ex;
+  background: #EFF;
+}
+
+.cardlist .card {
+  display: block;
+  width: 130ex;
+  border-style: solid;
+  border-width: 1px;
+  margin: .25em auto;
+  padding: 0;
+  overflow: auto;
+  background: #FFF;
+}
+
+.cardlist .card .section {
+  display: inline-block;
+  float:left;
+  width: 20ex;
+  margin: .125em .25ex .5ex .25ex;
+  padding: 0 .2em .2em .2em;
+  background: #EEE;
+}
+.cardlist .card .section a.attendance {
+  display: inline-block;
+  margin-right: 1ex;
+  word-wrap: break-word;
+}
+
+.cardlist .card .attendance {
+  width: 83.5ex;
+}
+.cardlist .card .attendance .check {
+  display: inline-block;
+  width: 27ex;
+}
+
+.cardlist .card .control {
+  float: right;
+  text-align: right;
+  margin-right: 0;
+  background: #EEF;
+}
+.cardlist .card .control .item {
+  color: #008;
+  margin-top: .2em;
+  margin-right: 1ex;
+}
+.cardlist .card .control a.item {
+  min-width: 10ex;
+  border-style: solid double solid solid;
+  border-width: 1px 3px 1px 1px;
+  border-color: #000;
+  padding: .1em 1ex;
+  background: #FFF;
+}
+.cardlist .card .control a.item:hover{
+  border-width: 1px 1px 1px 1px;
+}
+
+.cardlist .card .section .sectitle {
+  display: block;
+  font-weight: bold;
+  margin: .2em .2em .2em .2em;
+}
+
+.cardlist .card .section .item {
+  display: block;
+  max-width: 20ex;
+  word-wrap: break-word;
+}
+
+.cardlist .card .section textarea.NOTE {
+  min-height: 6em;
+}
+.cardlist .card .section textarea.ADR {
+  min-height: 4em;
+}
+
+.cardlist .card .section .PHOTO {
+  width: 20ex;
+  max-heigth: 30ex;
+}
+
+.cardlist .card .section .FN {
+  font-weight: bold;
+  font-size: 1.2em;
+}
+
diff --git a/email/email.html.sh b/email/email.html.sh
new file mode 100755 (executable)
index 0000000..7f93893
--- /dev/null
@@ -0,0 +1,57 @@
+# Copyright 2014 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+cat <<EOF
+<div class="select">
+  <span class="label">$(l10n filter_label)</span>
+  <form class="search" action="?action=select_mailing" method="POST">
+
+    <div class="selectlist">
+      <span class="label">$(l10n select_courses):</span>
+      <select name="courses" multiple="multiple">
+       $(listcourses |while read course; do
+         name="$(sed -rn 's:^SUMMARY(;.+)*\:(.*)\r$:\2:p' "$_DATA/ical/$course")"
+         echo '<option value="'$course'">'$name'</option>'
+       done)
+      </select>'
+    </div>
+
+    <div class="selectlist">
+      <span class="label">$(l10n select_attendees):</span>
+      <select name="courses" multiple="multiple">
+       $(listcards |while read card; do
+         n_last="$(  sed -rn 's:^N(;.+)*\:([^;]*;){0} *([^;]*).*$:\3:p' "$_DATA/vcard/$card")"
+         n_first="$( sed -rn 's:^N(;.+)*\:([^;]*;){1} *([^;]*).*$:\3:p' "$_DATA/vcard/$card")"
+         n_middle="$(sed -rn 's:^N(;.+)*\:([^;]*;){2} *([^;]*).*$:\3:p' "$_DATA/vcard/$card")"
+         n_pre="$(   sed -rn 's:^N(;.+)*\:([^;]*;){3} *([^;]*).*$:\3:p' "$_DATA/vcard/$card")"
+         n_post="$(  sed -rn 's:^N(;.+)*\:([^;]*;){4} *([^;]*).*$:\3:p' "$_DATA/vcard/$card")"
+         name="${n_pre} ${n_first} ${n_middle} ${n_last} ${n_post}"
+         echo '<option value="'$card'">'$name'</option>'
+       done)
+      </select>'
+    </div>
+
+    <button type="submit" name="choice" value="new_selection">$(l10n selection_apply)</button>
+    <button type="submit" name="choice" value="del_selection">$(l10n selection_clear)</button>
+  </form>
+</div>
+
+<div class="maillist">
+</div>
+EOF
+
+# vi:set filetype=html:
diff --git a/email/email.sh b/email/email.sh
new file mode 100755 (executable)
index 0000000..c79876f
--- /dev/null
@@ -0,0 +1,34 @@
+#!/bin/zsh
+
+# Copyright 2014 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+[ -z "${_GET[order]}" ] && _GET[order]=DOW
+
+listcards() {
+  ls -1 ${_DATA}/vcard/*vcf 2>/dev/null |while read file; do
+    fn=$(sed -rn 's:^N(;.+)*\:([^;]*;){1} *([^;]*).*$:\3:p' "$file")
+    echo "$fn\t$file"
+  done |sort |sed -r 's:^.*\t(.*/)([^/]+)$:\2:'
+}
+
+listcourses() {
+  ls -1 ${_DATA}/ical/*ics |while read file; do
+    icstime="$(sed -rn 's:^DTSTART\:(TZID=.*\:)?([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})Z?\r$:\2-\3-\4 \5\:\6\:\7:p' "$file")"
+    echo "$(date -d "$icstime" "+%u %H%M%S")\t$file"
+  done |sort |sed -r 's:^.*\t(.*/)([^/]+)$:\2:'
+}
diff --git a/frame.html.sh b/frame.html.sh
new file mode 100755 (executable)
index 0000000..6ba48b5
--- /dev/null
@@ -0,0 +1,32 @@
+#!/bin/zsh
+
+# Copyright 2014, 2015, 2017 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+cat <<EOF
+[!DOCTYPE HTML]
+
+[html [head [title $(l10n p_${PAGE}) ]
+  [meta name="viewport" content="width=device-width"]
+  [link rel="stylesheet" href="/common.css"]
+  [link rel="stylesheet" href="?static=${PAGE}.css"]
+] [body class="$PAGE" id="CONFIGURE"
+  [div class="NAVIGATION"
+    [a "/cards/" Cards]
+  ]
+]]'
+EOF
diff --git a/globals.sh b/globals.sh
new file mode 100755 (executable)
index 0000000..36498e1
--- /dev/null
@@ -0,0 +1,51 @@
+#!/bin/zsh
+
+# Copyright 2014 - 2016 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+date() {
+  LANG=de_DE.UTF-8 LC_ALL=de_DE.UTF-8 /bin/date $@
+}
+
+PROFILE=$(validate "$PROFILE" 'medical|circus' circus )
+
+PAGE="${_GET[p]:-${PAGE:-cards}}"
+
+uuidgenerator(){
+  head -c16 /dev/urandom |sha1sum - |cut -c1-32
+}
+
+_checked=''
+checked(){
+  if [ "$_checked" = "$1" ]; then
+    printf 'checked="checked"'
+  fi
+}
+
+
+VCF_FIELDS=(PHOTO LOGO FN NICKNAME SOUND GENDER KIND TITLE ROLE ORG MEMBER CATEGORIES ANNIVERSARY BDAY EMAIL TEL IMPP ADR URL LANG NOTE RELATED X-ZACK-JOINDATE X-ZACK-LEAVEDATE X-HEALTH-INSURANCE X-HEALTH-INSURANCE-NOCONTRIB X-CLIENT-REFERRAL)
+
+case "${PROFILE}" in
+medical)
+  data_dirs vcard cache temp mappings prescriptions therapies
+  [ -z "$NAVIGATION" ] && NAVIGATION=(cards)
+;;
+circus)
+  data_dirs vcard ical cache temp mappings
+  [ -z "$NAVIGATION" ] && NAVIGATION=(cards courses)
+;;
+esac
diff --git a/index.cgi b/index.cgi
new file mode 100755 (executable)
index 0000000..9d74909
--- /dev/null
+++ b/index.cgi
@@ -0,0 +1,81 @@
+#!/bin/sh
+
+for n in "$@"; do case ${n%%=*} in
+  data) _DATA="${n#data=}";;
+  exec) _EXEC="${n#exec=}";;
+  noerr) exec 2>&-;;
+esac; done
+
+[ -z "${_EXEC%/}" ] && _EXEC="$(realpath "${0%/*}")" || _EXEC="${_EXEC%/}"
+[ -z "${_DATA%/}" ] && _DATA=. || _DATA="${_DATA%/}"
+
+debug() {
+  local dbg=/dev/stderr
+  if [ ! "$DEBUG" ]; then
+    [ "$#" -gt 0 ] && : || cat;
+  elif [ "$#" -gt 0 ]; then
+    printf '%s\n' "$@" >>"$dbg"
+  else
+    tee -a "$dbg"
+  fi
+}
+
+. "$_EXEC/cgilite/cgilite.sh"
+. "$_EXEC/cgilite/session.sh"
+
+_PATH="$(PATH "/${PATH_INFO}")"
+ACTION="$(GET a)"
+
+message="$(COOKIE message)"
+[ "$message" ] && SET_COOKIE 0 message=''
+
+checked(){
+  if [ "$1" = "$2" ] || [ "$1" -eq "$2" ]; then
+    printf 'checked="checked"'
+  fi 2>/dev/null
+}
+selected(){
+  if [ "$1" = "$2" ] || [ "$1" -eq "$2" ]; then
+    printf 'selected="selected"'
+  fi 2>/dev/null
+}
+
+yield_page() {
+  local class="$1" style="$2"
+  printf 'Content-Type: text/html; charset=utf-8\r\n\r\n'
+  { printf '
+       [html [head
+          [title Confetti]
+         [meta name="viewport" content="width=device-width"]
+         [link rel="stylesheet" type="text/css" href="/style.css"]
+    '
+    [ -n "$style" ] && printf '
+         [link rel="stylesheet" type="text/css" href="%s"]
+    ' "$style"
+    printf '
+       ] [body #top class="%s"
+    ' "$class"
+    cat
+    [ "$message" ] && printf '[p #message\n%s\n]' $(l10n "$message")
+    printf '] ]'
+  } \
+  | "${_EXEC}/cgilite/html-sh.sed"
+}
+
+topdir="${_PATH#/}"
+topdir="/${topdir%%/*}"
+
+if [ "${_PATH}" = / ]; then
+  REDIRECT /cards/
+elif   [   -d "${_EXEC}/${_PATH}" -a -x "${_EXEC}/${_PATH}/index.cgi" ]; then
+  . "${_EXEC}/${_PATH}/index.cgi"
+elif [ ! -d "${_EXEC}/${_PATH}" -a -x "${_EXEC}/${_PATH}" ]; then
+  . "${_EXEC}/${_PATH}"
+elif [ ! -x "${_EXEC}/${_PATH}" -a -r "${_EXEC}/${_PATH}" ]; then
+  . "$_EXEC/cgilite/file.sh"
+  FILE "${_EXEC}/${_PATH}"
+elif   [   -d "${_EXEC}/${topdir}" -a -x "${_EXEC}/${topdir}/index.cgi" ]; then
+  . "${_EXEC}/${topdir}/index.cgi"
+else
+  printf 'Status: 404 Not Found\r\nContent-Length: 0\r\n\r\n'
+fi
diff --git a/pdiread.sh b/pdiread.sh
new file mode 100755 (executable)
index 0000000..8118318
--- /dev/null
@@ -0,0 +1,186 @@
+#!/bin/zsh
+
+# Copyright 2014 - 2018 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+# This is a parsing library for the Personal Data Interchange format (PDI)
+# PDI is the format for encoding VCard (.vcf) and iCalendar (.ics) files
+
+[ -n "$include_pdi" ] && return 0
+include_pdi="$0"
+
+BR='
+'
+
+pdi_load() {
+  # normalise PDI file for processing with pdi_* functions
+  # functions in this library can only be applied to normalised data
+  # Usage example:
+  # data="$(pdi_load file.vcf)"
+
+  sed -En '
+    # === Read entire file into buffer ===
+    :X $bY; N; bX; :Y s;^.*$;\n&\n;;
+
+    # === Join continuing lines, strip trailing CRs ===
+    s;\r*\n[ \t];;g;
+    s;\r*\n;\n;g;
+
+    # === turn property names to upper case, strip group names ===
+    s;\n([^;:\.\n]+\.)([^;:\n]+);\n\2;g;
+    :upcase
+    s;(\n[^;:]*)a;\1A;g; s;(\n[^;:]*)b;\1B;g; s;(\n[^;:]*)c;\1C;g; s;(\n[^;:]*)d;\1D;g; s;(\n[^;:]*)e;\1E;g;
+    s;(\n[^;:]*)f;\1F;g; s;(\n[^;:]*)g;\1G;g; s;(\n[^;:]*)h;\1H;g; s;(\n[^;:]*)i;\1I;g; s;(\n[^;:]*)j;\1J;g;
+    s;(\n[^;:]*)k;\1K;g; s;(\n[^;:]*)l;\1L;g; s;(\n[^;:]*)m;\1M;g; s;(\n[^;:]*)n;\1N;g; s;(\n[^;:]*)o;\1O;g;
+    s;(\n[^;:]*)p;\1P;g; s;(\n[^;:]*)q;\1Q;g; s;(\n[^;:]*)r;\1R;g; s;(\n[^;:]*)s;\1S;g; s;(\n[^;:]*)t;\1T;g;
+    s;(\n[^;:]*)u;\1U;g; s;(\n[^;:]*)v;\1V;g; s;(\n[^;:]*)w;\1W;g; s;(\n[^;:]*)x;\1X;g; s;(\n[^;:]*)y;\1Y;g;
+    s;(\n[^;:]*)z;\1Z;g;
+    t upcase;
+
+    # === Insert empty attribute fields where no attributes are present ===
+    s;\n([^;:]+):;\n\1\;:;g;
+
+    # === Unscramble aggregated fields ===
+    :disag
+    s;\n([^:\n]+:)(([^\n]*[^\])?(\\\\)*),;\n\1\2\n\1;;
+    t disag;
+
+    # === Insert FN when only N is present ===
+    /\nFN[;:]/!{
+      s,\nN(;[^:]*)?:([^;\n]*);([^;\n]*);([^;\n]*);([^;\n]*);([^;\n]*);?\n,&FN;:\5 \3 \4 \2 \6\n,;
+      :despace
+      s,(\nFN;:[^\n]*)  ([^\n]*\n),\1 \2,;
+      s,(\nFN;:) ([^\n]*\n),\1\2,;
+      s,(\nFN;:[^\n]*) (\n),\1\2,;
+      t despace;
+    }
+    /\nFN[;:]/!{ s,\n(N[;:][^\n]*)\n,&F\1\n,; }  # Fallback
+
+    # === Normalise various known vendor properties ===
+                s;\nX-MS-CARDPICTURE(\;|:);\nPHOTO\1;g;
+                        s;\nX-GENDER(\;|:);\nGENDER\1;g;
+                   s;\nX-ANNIVERSARY(\;|:);\nANNIVERSARY\1;g;
+         s;\nX-EVOLUTION-ANNIVERSARY(\;|:);\nANNIVERSARY\1;g;
+    s;\nX-KADDRESSBOOK-X-ANNIVERSARY(\;|:);\nANNIVERSARY\1;g;
+            s;\nX-EVOLUTION-BLOG-URL(\;|:);\nURL\1;g;
+                           s;\nAGENT(\;|:);\nRELATED\;VALUE=text\;TYPE=agent\1;g;
+                     s;\nX-ASSISTANT(\;|:);\nRELATED\;VALUE=text\;TYPE=assistant\1;g;
+           s;\nX-EVOLUTION-ASSISTANT(\;|:);\nRELATED\;VALUE=text\;TYPE=assistant\1;g;
+ s;\nX-KADDRESSBOOK-X-ASSISTANTSNAME(\;|:);\nRELATED\;VALUE=text\;TYPE=assistant\1;g;
+                       s;\nX-MANAGER(\;|:);\nRELATED\;VALUE=text\;TYPE=manager\1;g;
+             s;\nX-EVOLUTION-MANAGER(\;|:);\nRELATED\;VALUE=text\;TYPE=manager\1;g;
+   s;\nX-KADDRESSBOOK-X-MANAGERSNAME(\;|:);\nRELATED\;VALUE=text\;TYPE=manager\1;g;
+                        s;\nX-SPOUSE(\;|:);\nRELATED\;VALUE=text\;TYPE=spouse\1;g;
+              s;\nX-EVOLUTION-SPOUSE(\;|:);\nRELATED\;VALUE=text\;TYPE=spouse\1;g;
+     s;\nX-KADDRESSBOOK-X-SPOUSENAME(\;|:);\nRELATED\;VALUE=text\;TYPE=spouse\1;g;
+
+    # === Normalise obsolete vendor IM properties ===
+            s;\nX-AIM((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;\nIMPP\1:aim:;g;
+            s;\nX-ICQ((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;\nIMPP\1:aim:;g;
+    s;\nX-GOOGLE-TALK((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;\nIMPP\1:xmpp:;g;
+         s;\nX-JABBER((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;\nIMPP\1:xmpp:;g;
+            s;\nX-MSN((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;\nIMPP\1:msn:;g;
+          s;\nX-YAHOO((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;\nIMPP\1:ymsgr:;g;
+            s;\nX-SIP((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):(sip:)?;\nIMPP\1:sip:;g;
+
+    # === Update obsolete LABEL property ===
+    s;\nLABEL((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):(.*)\n;\nADR\1\;LABEL="\5":\n;g;
+
+    p;' "$1"
+}
+
+pdi_count(){
+  local card="$1" name="$2" rc='' cnt=0
+  while rc="${card#*${BR}${name};}"; do
+    [ "${rc}" != "${card}" ] || break
+    card="$rc"
+    cnt=$(($cnt + 1))
+  done
+  printf %i\\n $cnt
+}
+
+pdi_attrib(){
+  local card=":$1" name="$2" cnt="${3:-1}" attr="$4"
+  while [ $cnt -gt 0 ]; do
+    [ "${card#*${BR}${name};}" = "$card" ] && return 1
+    card="${card#*${BR}${name};}"
+    cnt=$((cnt - 1))
+  done
+  card="${card%%:*}"
+  if [ "$attr" ]; then
+    case $card in
+      *\;"$attr"=*) card="${card#*;${attr}=}";;
+      "$attr"=*) card="${card#${attr}=}";;
+      "$attr"|*\;"$attr"|"$attr"\;*|*\;"$attr"\;*) return 0;;
+      *) return 1;;
+    esac
+    case $card in
+      \"*\"\;*|\'*\'\;*)
+        card="${card#[\"\']}"; card="${card%%[\"\'];*}"
+        ;;
+      \"*\"|\'*\')
+        card="${card#[\"\']}"; card="${card%%[\"\']}"
+        ;;
+      *\;*) card="${card%%;*}";;
+    esac
+  fi
+  printf %s\\n "${card}"
+}
+
+pdi_value(){
+  local card="${BR}$1" name="$2" cnt="${3:-1}"
+  while [ "$cnt" -gt 0 ]; do
+    [ "${card#*${BR}${name};*:}" = "$card" ] && return 1
+    card="${card#*${BR}${name};*:}"
+    cnt=$((cnt - 1))
+  done
+  printf %s\\n "${card%%${BR}*}"
+}
+
+pdi_update_value(){
+  local card="${BR}$1" name="$2" cnt="$3" val="$4"
+  while [ "$cnt" -gt 0 ]; do
+    if [ "${card#*${BR}${name};*:}" = "${card}" ]; then
+       printf '%s\n%s;:' "${card%${BR}END;:VCARD*}" "${name}"
+       card="${BR}END;:VCARD"
+       break;
+    else
+       printf '%s\n%s;' "${card%%${BR}${name};*}" "${name}"
+       card="${card#*${BR}${name};}"
+       printf '%s:' "${card%%:*}"
+       card="${card#*:}"
+    fi
+    cnt=$((cnt - 1))
+  done
+  printf '%s\n%s\n' "$val" "${card#*${BR}}"
+}
+
+pdi_update_attrib(){
+  local card="${BR}$1" name="$2" cnt="$3" val="$4"
+  while [ "$cnt" -gt 0 ]; do
+    if [ "${card#*${BR}${name};*:}" = "${card}" ]; then
+       printf '%s\n%s;' "${card%${BR}END;:VCARD*}" "${name}"
+       card=":${BR}END;:VCARD"
+       break;
+    else
+       printf '%s\n%s;' "${card%%${BR}${name};*}" "${name}"
+       card="${card#*${BR}${name};}"
+    fi
+    cnt=$((cnt - 1))
+  done
+  printf '%s:%s\n' "$val" "${card#*:}"
+}
diff --git a/prescriptions/edit_prescription.sh b/prescriptions/edit_prescription.sh
new file mode 100755 (executable)
index 0000000..b7a9af4
--- /dev/null
@@ -0,0 +1,175 @@
+# Copyright 2016 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+check(){ [ "$1" = "$2" ] && printf checked}
+[ -z $mpx[presctype] ] && mpx[presctype]=doctor_compulsory
+
+cat <<END_HTML
+<form class="prescription" action="?action=update_prescription" method="POST">
+<input type="hidden" name="prescription" value="${mpx[prescription]}">
+
+<label class=presctype>$(l10n doctor):</label>
+<input type=radio name=presctype id=doctor_selfpaid value=doctor_selfpaid $(check "$mpx[presctype]" doctor_selfpaid)>
+<label for=doctor_selfpaid>$(l10n selfpaid)</label>
+<input type=radio name=presctype id=doctor_private value=doctor_private $(check "$mpx[presctype]" doctor_private)>
+<label for=doctor_private>$(l10n private)</label>
+<input type=radio name=presctype id=doctor_compulsory value=doctor_compulsory $(check "$mpx[presctype]" doctor_compulsory)>
+<label for=doctor_compulsory>$(l10n compulsory)</label>
+<br>
+
+<label class=presctype>$(l10n dentist):</label>
+<input type=radio name=presctype id=dentist_selfpaid value=dentist_selfpaid $(check "$mpx[presctype]" dentist_selfpaid)>
+<label for=dentist_selfpaid>$(l10n selfpaid)</label>
+<input type=radio name=presctype id=dentist_private value=dentist_private $(check "$mpx[presctype]" dentist_private)>
+<label for=dentist_private>$(l10n private)</label>
+<input type=radio name=presctype id=dentist_compulsory value=dentist_compulsory $(check "$mpx[presctype]" dentist_compulsory)>
+<label for=dentist_compulsory>$(l10n compulsory)</label>
+<br>
+
+<label class=presctype>$(l10n noprescription):</label>
+<input type=radio name=presctype id=noprescription_selfpaid value=noprescription_selfpaid $(check "$mpx[presctype]" noprescription_selfpaid)>
+<label for=noprescription_selfpaid>$(l10n selfpaid)</label>
+<br>
+
+<label class=presctype>$(l10n altpractition):</label>
+<input type=radio name=presctype id=altpractition_selfpaid value=altpractition_selfpaid $(check "$mpx[presctype]" altpractition_selfpaid)>
+<label for=altpractition_selfpaid>$(l10n selfpaid)</label>
+<input type=radio name=presctype id=altpractition_private value=altpractition_private $(check "$mpx[presctype]" altpractition_private)>
+<label for=altpractition_private>$(l10n private)</label>
+<br>
+
+<fieldset class="baseinfo">
+<label for="insurance">$(l10n insurance)</label>
+<input id="insurance" name="insurance" value="${mpx[insurance]}" placeholder="$(l10n insurance)">
+<br>
+<label for="name">$(l10n name)</label>
+<label for="bday">$(l10n bday)</label>
+<br>
+<textarea id="name" name="name" placeholder="$(l10n name)">${mpx[name]}</textarea>
+<input id="bday" name="bday" value="${mpx[bday]}" placeholder="$(l10n bday)">
+
+<br>
+<label for="date">$(l10n date)</label>
+<input id="date" name="date" value="${mpx[date]}" placeholder="$(l10n date)">
+</fieldset>
+
+<fieldset class="misc">
+<h1 id="${mpx[prescription]}">$(l10n therapy_prescription)</h1>
+
+<label for="addcontrib">$(l10n addcontrib)</label>
+<input id="addcontrib" name="addcontrib" value="${mpx[addcontrib]}" placeholder="$(l10n addcontrib)">
+<label for="contribconfirm">$(l10n contribconfirm)</label>
+<input id="contribconfirm" name="contribconfirm" value="${mpx[contribconfirm]}" placeholder="$(l10n contribconfirm)">
+<input type="checkbox" id="contribreceipt" name="contribreceipt" value="true" ${mpx[contribreceipt]:+checked}>
+<label for="contribreceipt">$(l10n contribreceipt)</label>
+</fieldset>
+
+<input type="checkbox" id="prescreviewed" name="prescreviewed" value="true" ${mpx[prescreviewed]:+checked}>
+<label for="prescreviewed">$(l10n prescreviewed)</label>
+
+<fieldset class="catalogue">
+<h2>$(l10n prescription_by_catalogue)</h2>
+<input type="radio" id="prescfirst" name="prescno" value="first" $(check "$mpx[prescno]" first)>
+<label for="prescfirst">$(l10n prescfirst)</label>
+<br>
+<input type="radio" id="prescfollow1" name="prescno" value="follow1" $(check "$mpx[prescno]" follow1)>
+<label for="prescfollow1">$(l10n prescfollow1)</label>
+<br>
+<input type="radio" id="prescfollow2" name="prescno" value="follow2" $(check "$mpx[prescno]" follow2)>
+<label for="prescfollow2">$(l10n prescfollow2)</label>
+<br>
+<input type="radio" id="prescother" name="prescno" value="other" $(check "$mpx[prescno]" other)>
+<label for="prescother">$(l10n prescother)</label>
+<br>
+<input type="radio" id="presccontinual" name="prescno" value="continual" $(check "$mpx[prescno]" continual)>
+<label for="presccontinual">$(l10n presccontinual)</label>
+
+<br>
+<input type="checkbox" id="grouptherapy" name="grouptherapy" value="true" ${mpx[grouptherapy]:+checked}>
+<label for="grouptherapy">$(l10n grouptherapy)</label>
+<br>
+<input type="checkbox" id="housecall" name="housecall" value="true" ${mpx[housecall]:+checked}>
+<label for="housecall">$(l10n housecall)</label>
+<br>
+<input type="checkbox" id="report" name="report" value="true" ${mpx[report]:+checked}>
+<label for="report">$(l10n report)</label>
+</fieldset>
+
+<fieldset class="description">
+<label for="quantity">$(l10n quantity)</label>
+<label for="remidy">$(l10n remidy)</label>
+<label for="quantity_weekly">$(l10n quantity_weekly)</label>
+<p>
+<input id="quantity" name="quantity" value="${mpx[quantity]}" placeholder="$(l10n quantity)">
+<textarea id="remidy" name="remidy" placeholder="$(l10n remidy)">${mpx[remidy]}</textarea>
+<input id="quantity_weekly" name="quantity_weekly" value="${mpx[quantity_weekly]}" placeholder="$(l10n quantity_weekly)">
+</p>
+
+$( for n in {0..10}; do
+  if [ "$n" -eq 0 -o -n "${mpx[quantity$n]}" -o -n "${mpx[remidy$n]}" -o -n "${mpx[quantity_weekly$n]}" ]; then
+    printf '<input class="trailbtn" type="checkbox" checked="checked" />'
+  else
+    printf '<input class="trailbtn" type="checkbox" />'
+  fi
+  printf '<p class="trailbox">
+    <input class="quantity" name="quantity" placeholder="%s" value="%s">
+    <textarea class="remidy" name="remidy" placeholder="%s">%s</textarea>
+    <input class="quantity_weekly" name="quantity_weekly" placeholder="%s" value="%s">
+    </p>
+  ' "$(l10n quantity)" "${mpx[quantity$n]}" \
+    "$(l10n remidy)" "${mpx[remidy$n]}" \
+    "$(l10n quantity_weekly)" "${mpx[quantity_weekly$n]}"
+done )
+
+<br>
+<p class="indicator_codes">
+<label for="indicator">$(l10n indicator)</label>
+<input id="indicator" name="indicator" value="${mpx[indicator]}" placeholder="$(l10n indicator)">
+<br>
+<label for="icd10">$(l10n icd10)</label>
+<input id="icd10" name="icd10" value="${mpx[icd10]}" placeholder="$(l10n icd10)">
+</p>
+<br>
+<p class="indicator_reading">
+<label for="indicator_reading">$(l10n indicator_reading)</label>
+<textarea id="indicator_reading" name="indicator_reading" placeholder="$(l10n indicator_reading)">${mpx[indicator_reading]}</textarea>
+</p>
+<br>
+<p class="issuer">
+  <label>$(l10n issuer)</label>
+  <input type="radio" name="issuer_select" value="list" id="issuer_select_list" checked /><label for="issuer_select_list">$(l10n issuer_from_list)</label><!--
+  --><input type="radio" name="issuer_select" value="other" id="issuer_other"><label for="issuer_other">$(l10n issuer_other)</label>
+  <select class="item" name="issuer">
+    <option value="" disabled="disabled" $([ -z "${mpx[issuer]}" ] && printf 'selected' )>$(l10n issuer)...</option>
+    $(list_prescription_issuers |while read f; do
+      [ "$f" = "$mpx[issuer]" ] \
+      && printf '<option value="%s" selected>%s</option>' "$f" "$f" \
+      || printf '<option value="%s">%s</option>' "$f" "$f"
+    done)
+  </select>
+  <input type="text" name="issuer_other" value="" placeholder="$(l10n issuer)..." />
+</p>
+</fieldset>
+
+<fieldset class="controls">
+<button type="submit" name="action" value="save">$(l10n save)</button>
+<button type="submit" name="action" value="cancel">$(l10n cancel)</button>
+<button type="submit" name="action" value="delete">$(l10n delete)</button>
+</fieldset>
+
+</form>
+END_HTML
diff --git a/prescriptions/new_prescription.sh b/prescriptions/new_prescription.sh
new file mode 100755 (executable)
index 0000000..26173af
--- /dev/null
@@ -0,0 +1,38 @@
+#!/bin/zsh
+
+# Copyright 2016 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+cgi_refdata
+
+client="${_POST[client]:-${_GET[client]}}"
+
+uid=$(uuidgenerator)
+prescription="${client%.vcf}.${uid}.mpx"
+
+cardfile="$_DATA/vcard/$client"
+tempfile="$_DATA/temp/$prescription"
+
+cat >"$tempfile" <<EOF
+prescription:${prescription}
+insurance:$(sed -nr 's;^X-HEALTH-INSURANCE:(.*)\;.*\;\r?$;\1;p' "$cardfile")
+bday:$(sed -nr 's;^BDAY:(.*)\r?$;\1;p' "$cardfile")
+name:$(sed -rn '/^N[\;:]/{s;^N(\;[^:]*)?:([^\;]*)(\;[^\;]*)(\;[^\;]*)?(\;[^\;]*)?(\;[^\;]*)?\r?$;\5 \3 \4 \2 \6;;s;[\;,]; ;g;s; +; ;g;s;^ | $;;g;p}' "$cardfile")\n$(sed -nr 's;^ADR:(.*)\r?$;\1;p' "$cardfile")
+date:
+EOF
+
+echo -n "Location: ?p=prescriptions&edit=$prescription\n\n"
diff --git a/prescriptions/prescriptions.css b/prescriptions/prescriptions.css
new file mode 100644 (file)
index 0000000..6eebe24
--- /dev/null
@@ -0,0 +1,298 @@
+/*
+# Copyright 2016 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+*/
+
+@import url("?static=cards.css");
+
+body {padding-bottom: 3em; }
+
+.trailbtn { display: none; }
+.trailbtn + .trailbox { display: none; }
+.trailbtn:checked + .trailbox { display: inline-block; }
+.trailbtn:checked + .trailbox + .trailbtn { display: block; }
+.trailbtn:checked + .trailbox + .trailbtn:before {
+   display: block; content: '+';
+   width: 3ex; text-align: center;
+   margin-top: .25em; padding: .25em 0;
+   background-color: #FFF;
+   border-width: 1px; border-style: solid;
+}
+.trailbtn:checked + .trailbox + .trailbtn:checked,
+.trailbtn:checked { display: none; }
+
+.prescription {
+  display: inline-block;
+  width: 96%; max-width: 460px;
+  color: #800;
+  background-color: #DDD;
+  margin: 1em -1% 0 2%; padding: 0;
+  border: 1px solid #888;
+  overflow: hidden;
+  vertical-align: top;
+}
+form.prescription { padding-top: 1ex;}
+
+.newprescription {
+  display: block;
+  margin: 0 2em; padding: .5ex 2ex;
+  background-color: #CFF;
+  border: 1px solid #888;
+  border-radius: 0 0 1ex 1ex;
+}
+
+.prescription * {
+  display: inline-block;
+  font-size: 1em;
+  line-height: 1em;
+  margin: 0; padding: 0;
+}
+.prescription label {
+  padding-left: .5ex;
+  font-size: .75em;
+}
+
+.prescription fieldset {
+  display: inline-block;
+  margin: 0; padding: 1ex;
+  margin-right: -.625ex;
+  border: none;
+  vertical-align: top;
+}
+.prescription fieldset br { display: none;}
+
+.prescription span,
+.prescription input,
+.prescription textarea {
+  height: 1.5em;
+  border: 1px solid #800;
+  padding: .25ex;
+  background-color: #FFF;
+}
+.prescription span {
+  background-color: #EEE;
+  padding: .5ex .25ex;
+  white-space: pre-wrap;
+  font-size: .75em;
+  overflow: hidden;
+}
+
+.prescription input[type=checkbox],
+.prescription input[type=radio] { display: none;}
+
+.prescription label.checkbox,
+.prescription label.radio,
+.prescription input[type=checkbox] + label,
+.prescription input[type=radio] + label { padding-left: 1.25em; font-size: 1em;}
+
+.prescription label.checkbox:before,
+.prescription label.radio:before,
+.prescription input[type=checkbox] + label:before,
+.prescription input[type=radio] + label:before {
+  display: inline-block;
+  position: absolute;
+  margin-left: -1.25em;
+  width: .75em; height: .75em;
+  background-color: #FFF;
+  border: 1px solid #800;
+  content: ' ';
+}
+.prescription label.radio:before,
+.prescription input[type=radio] + label:before { border-radius: .5em;}
+.prescription label.checkbox.checked:before,
+.prescription label.radio.checked:before,
+.prescription input[type=checkbox]:checked + label:before,
+.prescription input[type=radio]:checked + label:before { content: "\2713";}
+
+.prescription a.button,
+.prescription input[type=submit],
+.prescription button {
+  height: 1.5em;
+  color: #FFF;
+  background-color: #800;
+  text-align: center;
+  text-decoration: none;
+  margin: 0; padding: .125em 0 0 0;
+  border: none;
+}
+.prescription a.button {padding: .5ex;}
+
+/* ======== Specific ========== */
+
+.prescription label.presctype,
+.prescription input[name=presctype] + label {
+  font-size: medium;
+  width: 22%;
+  margin: 0; margin-right: -.5ex;
+  vertical-align: top;
+  padding: .25em .5ex .25em 3ex;
+  height: 2.5em;
+  border-top: 1px solid #DDD;
+}
+.prescription label.presctype {
+  text-align: right;
+  font-weight: bold;
+  font-size: .875em;
+  padding-right: 1ex;
+  padding-left: 0;
+}
+
+input[name=presctype][value\$=private]:checked  ~ fieldset,
+  input[name=presctype][value\$=private] + label,
+  .prescription.private { background-color: #CFC;}
+input[name=presctype][value\$=selfpaid]:checked  ~ fieldset,
+  input[name=presctype][value\$=selfpaid]         + label,
+  .prescription.selfpaid { background-color: #FFC;}
+input[name=presctype][value=doctor_compulsory]:checked  ~ fieldset,
+  input[name=presctype][value=doctor_compulsory]  + label,
+  .prescription.doctor.compulsory { background-color: #CFF;}
+input[name=presctype][value=dentist_compulsory]:checked  ~ fieldset,
+  input[name=presctype][value=dentist_compulsory] + label,
+  .prescription.dentist.compulsory { background-color: #FCC;}
+input[name=presctype][value^=altpractition]:checked  ~ fieldset,
+  input[name=presctype][value^=altpractition]     + label,
+  .prescription.altpractition { background-color: #FCF;}
+
+.prescription .baseinfo { width: 60%;}
+
+  .baseinfo label[for=insurance],
+  .baseinfo #insurance { width: 100%; }
+  .baseinfo label[for=name],
+  .baseinfo #name { width: 65%; margin-right: -.875ex;}
+  .baseinfo #name { height: 4em; }
+  .baseinfo label[for=bday],
+  .baseinfo #bday { width: 35%;}
+  .baseinfo #bday { height: 4em; text-align: center; vertical-align: top;}
+  .baseinfo label[for=date],
+  .baseinfo #date { width: 34%; margin-left: 65%;}
+  .baseinfo #date { text-align: right;}
+
+.prescription .misc { width: 40%; }
+
+  .misc h1 {
+    font-size: 1.25em;
+    font-weight: bold;
+    width: 100%;
+  }
+  .misc label[for=addcontrib],
+  .misc label[for=contribconfirm] {width: 100%;}
+  .misc #addcontrib,
+  .misc #contribconfirm {width: 100%; text-align: right;}
+
+.prescription label[for=prescreviewed] {
+  margin-left: 1ex;
+  font-weight: bold;
+  text-decoration: underline;
+  background-color: #FCC;
+}
+.prescription label[for=prescreviewed].checked,
+.prescription :checked + label[for=prescreviewed] {
+  font-weight: normal;
+  text-decoration: none;
+  background-color: transparent;
+}
+
+.prescription .catalogue { width: 100%; }
+
+  .catalogue h2:nth-of-type(1) {
+    font-weight: bold;
+    width: 100%;
+    margin-bottom: .25em;
+  }
+  .catalogue label {
+    display: inline-block;
+    width: 33%;
+    margin-right: -.625ex;
+    margin-top: .25em;
+    vertical-align: top;
+  }
+  .catalogue label[for=presccontinual] { margin-right: 33%;}
+
+.prescription .description { width: 100%; position: relative;}
+  .description * { margin-right: -.625ex; vertical-align: top; }
+  .description label {vertical-align: bottom;}
+
+  .description label[for=quantity] { width: 20%;}
+  .description label[for=remidy] { width: 60%; }
+  .description label[for=quantity_weekly] { width: 20%;}
+  .description #quantity,
+  .description .quantity { width: 20%;  height: 3em; text-align: center;}
+  .description #remidy,
+  .description .remidy { width: 60%; height: 3em;}
+  .description #quantity_weekly,
+  .description .quantity_weekly { width: 20%; height: 3em; text-align: center;}
+
+  .description .indicator_codes {display: inline-block; width: 20%; padding: 0; padding-top: 1.5ex;}
+  .description label[for=indicator],
+  .description label[for=icd10] { display: block; width: 100%;}
+  .description #icd10,
+  .description #indicator {width: 100%; text-align: right;}
+
+  .description .indicator_reading { display: inline-block; width: 78%; padding: 0; padding-top: 1.5ex;  margin-left: 2%;}
+  .description label[for=indicator_reading],
+  .description #indicator_reading { width: 100%; display: block;}
+  .description #indicator_reading { height: 4em;}
+
+.prescription .therapy_dates span { min-width: 8em; margin: 0 .5ex;}
+
+.prescription .issuer { display: inline-block; width: 50%; padding: 0; padding-top: 0; margin-left: 50%;}
+.prescription .issuer label:first-of-type {
+   display: block;
+   position: relative;
+   width: 50%; left: -50%; top: 2.25em;
+   font-size: 1em;
+   text-align: right;
+   padding-right: 1ex;
+ }
+.prescription .issuer input[type=radio] + label:before { content: none; }
+.prescription .issuer input[type=radio] { display: none; }
+.prescription .issuer input[type=radio] + label {
+  display: inline-block;
+  width: 50%;
+  padding: .25ex 0; margin: 0;
+  text-align: center;
+  border: 1px solid black;
+}
+.prescription .issuer input[type=radio]:checked + label {
+  font-weight: bold;
+  background-color: #FFF;
+  border-width: 1px;
+  border-bottom: 1px solid #FFF;
+}
+.prescription .issuer input[type=radio] + label + input + label + select,
+.prescription .issuer input[type=radio] + label + select + input { display: none; }
+.prescription .issuer input[type=radio]:checked + label + input + label + select,
+.prescription .issuer input[type=radio]:checked + label + select + input {
+  display: block; width: 100%;
+  border: 1px solid black;
+  background-color: #FFF;
+  border-width: 0 1px 1px 1px;
+  padding: .25ex .5ex;
+  margin-top: -1px;
+}
+.prescription .issuer input[type=radio]:checked + label + input + label + select option { display: block;}
+.prescription span#issuer { width: 100%; height: 3em; padding: 1ex 2ex;}
+
+.prescription .controls { width: 100%; }
+  .controls a.button,
+  .controls button[value=save],
+  .controls button[value=cancel],
+  .controls button[value=delete] { width: 25%;}
+
diff --git a/prescriptions/prescriptions.html.sh b/prescriptions/prescriptions.html.sh
new file mode 100755 (executable)
index 0000000..c4ef19a
--- /dev/null
@@ -0,0 +1,47 @@
+# Copyright 2016 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+client="${_GET[client]}"
+edit="${_GET[edit]}"
+[ -n "$edit" ] && client="${edit%.*.mpx}.vcf"
+
+cat <<EOF
+
+<div id="${card}" class="card">$(view_card "$client")</div>
+
+<!--h1>$(l10n prescriptions_current)</h1-->
+
+<div class="newprescription">
+  <form action="?action=new_prescription" method="POST">
+    <input type="hidden" name="client" value="$client">
+    <button type="submit">$(l10n newprescription)</button>
+  </form>
+</div>
+EOF
+
+list_prescriptions "$client" |grep -q "$edit" || edit_prescription "$edit"
+
+list_prescriptions "$client" \
+|while read pre; do 
+  [ "$pre" = "$edit" ] \
+  && edit_prescription "$pre" \
+  || view_prescription "$pre"
+done
+
+#<!--h1>$(l10n prescriptions_past)</h1-->
+
+# vi:set filetype=html:
diff --git a/prescriptions/prescriptions.sh b/prescriptions/prescriptions.sh
new file mode 100755 (executable)
index 0000000..fcef09a
--- /dev/null
@@ -0,0 +1,73 @@
+#!/bin/zsh
+
+# Copyright 2016 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+source "$_EXEC/pages/cards.sh"
+declare -A mpx
+
+BR='
+'
+
+view_card="$_EXEC/templates/view_client.sh"
+
+list_prescriptions(){
+  client="$1"
+  find "$_DATA/prescriptions/" -name "${client%.vcf}.*.mpx" \
+  | while read pfile; do
+    printf '%s\t%s\n' "$(grep '^date' "$pfile")" "${pfile##*/}"
+  done \
+  | sort -r | cut -f2
+}
+
+list_prescription_issuers(){
+  sed -rn 's;^issuer:(.+)$;\1;p' ${_DATA}/prescriptions/*.mpx \
+  | sort -u
+}
+
+edit_prescription(){
+  id="$1"
+  prescfile="$_DATA/prescriptions/$id"
+  tempfile="$_DATA/temp/$id"
+  [ -f "$tempfile" ] || cp "$prescfile" "$tempfile"
+
+  mpx=()
+  cat "$tempfile" |while read -r line; do
+    val="${line#*:}"
+    mpx[${line%%:*}]="${val//\\n/$BR}"
+  done
+
+  . "$_EXEC/templates/edit_prescription.sh"
+}
+
+view_prescription(){
+  id="$1"
+  prescfile="$_DATA/prescriptions/$id"
+
+  mpx=()
+  cat "$prescfile" |while read -r line; do
+    val="${line#*:}"
+    mpx[${line%%:*}]="$(htmlsafe "${val//\\n/$BR}")"
+  done
+
+  . "$_EXEC/templates/view_prescription.sh"
+}
+
+therapy_dates(){
+  tpyfile="$_DATA/therapies/${1%.mpx}.tpy"
+  sed -rn 's;^session[0-9]+_date:(.+)$;\1;p' "$tpyfile"
+}
diff --git a/prescriptions/text_prescriptions.sh b/prescriptions/text_prescriptions.sh
new file mode 100755 (executable)
index 0000000..8934bc7
--- /dev/null
@@ -0,0 +1,66 @@
+# Copyright 2016 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+. $_EXEC/templates/text_cards.sh
+
+item_name[therapy_prescription]="Heil&shy;mit&shy;tel&shy;ver&shy;ord&shy;nung"
+item_name[prescriptions_current]="Aktuelle Verordnungen"
+item_name[prescriptions_past]="Frühere Verordnungen"
+item_name[newprescription]="Neue Verordnung"
+item_name[date]="Datum"
+item_name[name]="Name d. Versicherten"
+item_name[bday]="geb. am"
+item_name[addcontrib]="Zuzahlung"
+item_name[contribconfirm]="Zuzahlung erfolgt am..."
+item_name[contribreceipt]="Quit&shy;tung heraus&shy;ge&shy;ge&shy;ben"
+item_name[prescreviewed]="Verordnung geprüft"
+item_name[prescreview]="Verordnung prüfen!"
+item_name[quantity]="Ver&shy;ord&shy;nungs&shy;men&shy;ge"
+item_name[remidy]="Heil&shy;mit&shy;tel nach Maß&shy;ga&shy;be des Ka&shy;ta&shy;lo&shy;ges"
+item_name[prescfirst]="Erst&shy;ver&shy;ord&shy;nung"
+item_name[prescfollow1]="1. Folge-&shy;VO"
+item_name[prescfollow2]="2. Folge-&shy;VO"
+item_name[prescother]="VO außer&shy;halb des Re&shy;gel&shy;falls"
+item_name[presccontinual]="Lang&shy;frist&shy;ver&shy;ord&shy;nung"
+item_name[grouptherapy]="Grup&shy;pen&shy;the&shy;ra&shy;pie"
+item_name[housecall]="Haus&shy;be&shy;such"
+item_name[report]="The&shy;ra&shy;pie&shy;be&shy;richt"
+item_name[indicator]="In&shy;di&shy;ka&shy;tions&shy;schlüssel"
+item_name[icd10]="ICD-10-Code"
+item_name[indicator_reading]="Befund Beschreibung"
+item_name[insurance]="Krankenkasse bzw. Kostenträger"
+item_name[prescription_by_catalogue]="Verordnung nach Maßgabe des Kataloges (Regelfall)"
+item_name[therapy_start]="Be&shy;hand&shy;lungs&shy;be&shy;ginn spä&shy;test. am"
+item_name[quantity_weekly]="An&shy;zahl pro Wo&shy;che"
+item_name[save]="Speichern"
+item_name[cancel]="Abbrechen"
+item_name[delete]="Löschen"
+item_name[therapy]="Zur Therapie"
+
+item_name[doctor]="Arzt"
+item_name[dentist]="Zahn&shy;arzt"
+item_name[altpractition]="Heil&shy;prak&shy;tiker"
+item_name[noprescription]="Ohne Ver&shy;ord&shy;nung"
+item_name[selfpaid]="Selbst&shy;zah&shy;lend"
+item_name[private]="Pri&shy;vat"
+item_name[compulsory]="Ge&shy;setz&shy;lich"
+
+item_name[therapy_dates]="Be&shy;hand&shy;lungs&shy;ter&shy;mi&shy;ne"
+
+item_name[issuer]="Ausgestellt durch"
+item_name[issuer_from_list]="Aus Liste"
+item_name[issuer_other]="Andere"
diff --git a/prescriptions/update_prescription.sh b/prescriptions/update_prescription.sh
new file mode 100755 (executable)
index 0000000..aeb4993
--- /dev/null
@@ -0,0 +1,55 @@
+#!/bin/zsh
+
+# Copyright 2016 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+BR='
+'
+prescription="${_POST[prescription]}"
+
+tempfile="$_DATA/temp/$prescription"
+prescfile="$_DATA/prescriptions/$prescription"
+client="${prescription%.*.mpx}.vcf"
+clientfile="$_DATA/vcard/$client"
+
+if [ -z "$prescription" -o \! -f "$clientfile" ]; then
+  redirect "?p=error"
+  exit 0
+fi
+
+[ "$_POST[issuer_select]" = "other" ] && _POST[issuer]="${_POST[issuer_other]}"
+
+# serialize POST array into file
+for key in ${(k)_POST}; do
+  printf %s:%s\\n "$key" "${_POST[$key]//$BR/\\n}"
+done >"$tempfile"
+
+case "${_POST[action]}" in
+  save)
+    mv "$tempfile" "$prescfile"
+    touch "$clientfile"
+    ;;
+  cancel)
+    rm "$tempfile"
+    ;;
+  delete)
+    rm "$tempfile" "$prescfile"
+    touch "$clientfile"
+    ;;
+esac
+
+redirect "?p=prescriptions&client=${client}#${prescription}"
diff --git a/prescriptions/view_prescription.sh b/prescriptions/view_prescription.sh
new file mode 100755 (executable)
index 0000000..ced7241
--- /dev/null
@@ -0,0 +1,121 @@
+# Copyright 2016 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+check(){ [ "$1" = "$2" ] && printf checked}
+
+cat <<END_HTML
+<div class="prescription ${mpx[presctype]%_*} ${mpx[presctype]#*_}">
+<fieldset class="baseinfo">
+<label for="insurance">$(l10n insurance)</label>
+<span id="insurance">${mpx[insurance]}</span>
+<br>
+<label for="name">$(l10n name)</label>
+<label for="bday">$(l10n bday)</label>
+<span id="name">${mpx[name]}</span>
+<span id="bday">${mpx[bday]}</span>
+
+<br>
+<label for="date">$(l10n date)</label>
+<span id="date">${mpx[date]}</span>
+</fieldset>
+
+<fieldset class="misc">
+<h1 id="${mpx[prescription]}">$(l10n therapy_prescription)</h1>
+<br>
+<label for="addcontrib">$(l10n addcontrib)</label>
+<span id="addcontrib">${mpx[addcontrib]}</span>
+<label for="contribconfirm">$(l10n contribconfirm)</label>
+<span id="contribconfirm">${mpx[contribconfirm]}</span>
+<label class="checkbox ${mpx[contribreceipt]:+checked}" for="contribreceipt">$(l10n contribreceipt)</label>
+</fieldset>
+
+<label class="checkbox ${mpx[prescreviewed]:+checked}" for="prescreviewed">
+  $([ -n "${mpx[prescreviewed]}" ] && printf %s "$(l10n prescreviewed)" \
+                                   || printf %s "$(l10n prescreview)" )
+</label>
+
+<fieldset class="catalogue">
+<h2>$(l10n prescription_by_catalogue)</h2>
+<label class="radio $(check "$mpx[prescno]" first)" for="prescfirst">$(l10n prescfirst)</label>
+<br>
+<label class="radio $(check "$mpx[prescno]" follow1)" for="prescfollow1">$(l10n prescfollow1)</label>
+<br>
+<label class="radio $(check "$mpx[prescno]" follow2)" for="prescfollow2">$(l10n prescfollow2)</label>
+<br>
+<label class="radio $(check "$mpx[prescno]" other)" for="prescother">$(l10n prescother)</label>
+<br>
+<label class="radio $(check "$mpx[prescno]" continual)" for="presccontinual">$(l10n presccontinual)</label>
+
+<br>
+<label class="checkbox ${mpx[grouptherapy]:+checked}" for="grouptherapy">$(l10n grouptherapy)</label>
+<br>
+<label class="checkbox ${mpx[housecall]:+checked}" for="housecall">$(l10n housecall)</label>
+<br>
+<label class="checkbox ${mpx[report]:+checked}" for="report">$(l10n report)</label>
+</fieldset>
+
+<fieldset class="description">
+  <label for="quantity">$(l10n quantity)</label>
+  <label for="remidy">$(l10n remidy)</label>
+  <label for="quantity_weekly">$(l10n quantity_weekly)</label>
+  <span id="quantity">${mpx[quantity]}</span>
+  <span id="remidy">${mpx[remidy]}</span>
+  <span id="quantity_weekly">${mpx[quantity_weekly]}</span>
+$( for n in {0..10}; do
+  [ -n "${mpx[quantity$n]}" -o -n "${mpx[remidy$n]}" -o -n "${mpx[quantity_weekly$n]}" ] \
+  && printf '
+    <span id="quantity">%s</span>
+    <span id="remidy">%s</span>
+    <span id="quantity_weekly">%s</span>
+    ' "${mpx[quantity$n]}" "${mpx[remidy$n]}" "${mpx[quantity_weekly$n]}"
+done )
+<br>
+<p class="indicator_codes">
+  <label for="indicator">$(l10n indicator)</label>
+  <span id="indicator">${mpx[indicator]}</span>
+  <br>
+  <label for="icd10">$(l10n icd10)</label>
+  <span id="icd10">${mpx[icd10]}</span>
+</p>
+<br>
+<p class="indicator_reading">
+  <label for="indicator_reading">$(l10n indicator_reading)</label>
+  <span id="indicator_reading">${mpx[indicator_reading]}</span>
+</p>
+</fieldset>
+
+<p class="therapy_dates">
+  <label>$(l10n therapy_dates):</label>
+  $(therapy_dates "$id" \
+    | while read date; do
+      printf '<span>%s</span>' "$date"
+    done
+  )
+</p>
+
+<p class="issuer">
+  <label>$(l10n issuer)</label>
+  <span id="issuer">$mpx[issuer]</span>
+</p>
+
+<fieldset class="controls">
+<a class="button" href="?p=prescriptions&amp;edit=${mpx[prescription]}#${mpx[prescription]}">$(l10n edit)</a>
+<a class="button" href="?p=therapy&amp;id=${mpx[prescription]%.mpx}.tpy">$(l10n therapy)</a>
+</fieldset>
+
+</div>
+END_HTML
diff --git a/session_lock.sh b/session_lock.sh
new file mode 100644 (file)
index 0000000..17af0e6
--- /dev/null
@@ -0,0 +1,60 @@
+#!/bin/sh
+
+[ "$include_session_lock" ] && return 0
+include_session_lock="$0"
+
+SLOCK(){
+  local file="$1";
+  local timeout="${2-900}"
+  local lockdir="$_DATA/lock/${file#$_DATA}"; lockdir="${lockdir%/}"
+  local ovlock="${lockdir%/*}/delete.${lockdir##*/}"
+  local tempfile="$lockdir/${SESSION_ID}"
+  local lockexpire=$(( $(date +%s) - timeout ))
+
+  mkdir -p "$_DATA/lock/${file%/*}"
+
+  if [ -e "$lockdir" ] \
+     && [ "$(stat -c %Y "$lockdir")" -lt "$lockexpire" ] \
+     && mkdir "$ovlock"; then
+    [ "$(stat -c %Y "$lockdir")" -lt "$lockexpire" ] \
+    && rm -r "$lockdir"
+    rmdir "$ovlock"
+  fi
+
+  printf '%s\n' "$tempfile"
+  if mkdir "$lockdir" 2>&-; then
+    cp "$file" "$tempfile"
+    return 0
+  else
+    return 1
+  fi
+}
+
+CHECK_SLOCK(){
+  local file="$1";
+  local lockdir="$_DATA/lock/${file#$_DATA}"; lockdir="${lockdir%/}"
+  local tempfile="$lockdir/${SESSION_ID}"
+
+  printf '%s\n' "$tempfile"
+  if [ -f "$tempfile" ]; then
+    touch "$lockdir"
+    return 0
+  else
+    return 1
+  fi
+}
+
+RELEASE_SLOCK(){
+  local file="$1";
+  local lockdir="$_DATA/lock/${file#$_DATA}"; lockdir="${lockdir%/}"
+  local ovlock="${lockdir%/*}/delete.${lockdir##*/}"
+  local tempfile="$lockdir/${SESSION_ID}"
+
+  if [ -f "$tempfile" ] && mkdir "$ovlock"; then
+    [ -f "$tempfile" ] && rm -r "$lockdir"
+    rmdir "$ovlock"
+    return 0
+  else
+    return 1
+  fi
+}
diff --git a/shcgi b/shcgi
new file mode 160000 (submodule)
index 0000000..8d60af4
--- /dev/null
+++ b/shcgi
@@ -0,0 +1 @@
+Subproject commit 8d60af41a80ea3760dd51c3bc34eab68d88b16ae
diff --git a/style.css b/style.css
new file mode 100644 (file)
index 0000000..33e7cd0
--- /dev/null
+++ b/style.css
@@ -0,0 +1,126 @@
+* {
+  font-family: sans-serif;
+  -moz-box-sizing: border-box;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+  padding: 0; margin: 0;
+  font-size: initial;
+  line-height: 1.25em;
+  z-index: 0;
+}
+
+input, textarea, select, select * {
+  font-size: .875em;
+  padding: .125em .125em;
+}
+input, select {
+  height: 1.5em;
+  vertical-align: bottom;
+}
+button { padding: 0 .5em; }
+
+body {
+  background: #EEE;
+  margin: 0;
+  padding: 0;
+  padding-top: 2.5em;
+  position: absolute;
+  min-height: 100%;
+  width: 100%;
+  padding-bottom: 2em;
+}
+.NAVIGATION {
+  position: fixed;
+  top: 0;
+  display: inline-block;
+  border-style: none solid solid solid;
+  border-width: 0 1px 1px 1px;
+  border-radius: 0 0 4px 4px;
+  margin: 0 2em;
+  padding: .5em 1ex;
+  background: #FFF;
+  z-index: 1;
+}
+.NAVIGATION label,
+.NAVIGATION a {
+  color: #008;
+  border: solid 1px #BBF;
+  margin: .5em .5ex .25em .5ex;
+  padding: .2em 3ex .2em 3ex;
+  background: #EFF;
+}
+.NAVIGATION label:hover,
+.NAVIGATION a:hover {
+  border-width: 2px 1px;
+  background: #F3FFFF;
+}
+
+#CONFIGURE        label[for="navigationconfig"] { display: none; }
+#CONFIGURE:target label[for="navigationconfig"] { display: inline; }
+
+.NAVIGATION input#navigationconfig { display: none; }
+.NAVIGATION input + form.config {
+  position: static;
+  display: none;
+}
+
+.NAVIGATION .config a { display: block; }
+.NAVIGATION .config a:hover { border-width: 1px 2px; }
+.NAVIGATION input:checked + form.config { display: block; }
+.NAVIGATION .config input[type=text],
+.NAVIGATION .config button {
+  font-size: .875em;
+  line-height: 1.5em;
+  height: 1.75em;
+  padding: 0 .25em;
+  vertical-align: bottom;
+  border: 1px solid #000;
+  background-color: #FFF;
+}
+.NAVIGATION .config input[type=text] {
+  margin: .25em 0 0 .375em;
+  border-right: none;
+  width: 70%;
+}
+.NAVIGATION .config button {
+  min-width: 2.5em;
+  text-align: center;
+  max-width: 30%;
+}
+.NAVIGATION .config button[value=del] {
+  background: #FCC;
+}
+
+#message {
+  display: block;
+  position: fixed;
+  top: 0; width: 100%;
+  margin: 0; padding: 1em;
+  text-align: center;
+  font-weight: bold;
+  background-color: #FAA;
+  border: 1px solid #000;
+  border-style: none none solid none;
+}
+
+.trailbtn { display: none; }
+.trailbtn + .trailbox { display: none; }
+.trailbtn:checked + .trailbox { display: inline-block; }
+.trailbtn:checked + .trailbox + .trailbtn { display: block; }
+.trailbtn:checked + .trailbox + .trailbtn:before {
+   display: block; content: '+';
+   width: 3ex; text-align: center;
+   margin-top: .25em; padding: .25em 0;
+   background-color: #FFF;
+   border-width: 1px; border-style: solid;
+}
+.trailbtn:checked + .trailbox + .trailbtn:checked,
+.trailbtn:checked { display: none; }
+
+#footer {
+  width: 100%;
+  background-color: #FFF;
+  border-top: 1px solid #BBF;
+  margin: 0; padding: .5em 2em;
+  position: absolute; bottom: 0;
+}
diff --git a/text_frame.sh b/text_frame.sh
new file mode 100755 (executable)
index 0000000..75e1511
--- /dev/null
@@ -0,0 +1,29 @@
+# Copyright 2014 - 2016, 2018 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+item_name[p_cards]="Personen"
+item_name[p_courses]="Kurse"
+item_name[p_email]="Email"
+item_name[p_error]="Fehler"
+item_name[p_categories]="Kategorien"
+item_name[p_prescriptions]="Verschreibungen"
+item_name[p_therapy]="Therapie"
+item_name[nc_edit]="&#x2699;"
+#item_name[nc_edit]="&#9776;"
+item_name[bm_add]="+"
+item_name[bm_del]="-"
+item_name[configure]="Anpassen"
diff --git a/therapy/text_therapy.sh b/therapy/text_therapy.sh
new file mode 100755 (executable)
index 0000000..a2735b2
--- /dev/null
@@ -0,0 +1,27 @@
+# Copyright 2016 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+. $_EXEC/templates/text_prescriptions.sh
+
+item_name[prescriptionlist]="Zur Verordnungsliste"
+item_name[delete_session]="Therapiesitzung entfernen"
+item_name[therapist]="Therapeut"
+item_name[number]="Nr."
+item_name[signature]="Un&shy;ter&shy;schrift"
+item_name[weekly]="p. Woche"
+item_name[notes]="Notizen"
+item_name[trailsave]="Speichern für weitere Felder"
diff --git a/therapy/therapy.css b/therapy/therapy.css
new file mode 100644 (file)
index 0000000..b17497c
--- /dev/null
@@ -0,0 +1,360 @@
+.trailbtn:checked + fieldset.trailbox { display: block;}
+
+.trailbtn:checked + .trailbox + .trailbtn {
+  display: block;
+  height: 2.25em; padding: 0 3ex;
+  font-size: 1em; font-weight: normal;
+  color: #000; background-color: #FDD;
+  border: 1px solid #000;
+  border-radius: 4px;
+}
+.trailbtn:checked + .trailbox + .trailbtn[type=submit]:before {content: none;}
+
+
+* { box-sizing: border-box; }
+body {
+  overflow: scroll;
+  position: relative;
+  width: 100%;
+  margin: 0; padding: 0;
+  padding-top: 2em;
+}
+
+form > button[type=submit] {
+  position: fixed; display: block;
+  top: 0; right: 2.5em;
+  height: 2.25em; padding: 0 3ex;
+  font-size: 1em; font-weight: bold;
+  color: #000; background-color: #FDD;
+  border-width: 1px; border-color: #000;
+  border-style: none solid solid solid;
+  border-radius: 0 0 4px 4px;
+  z-index: 3;
+}
+form > button[type=submit]:hover {
+  background-color: #FEE;
+}
+
+input.tab { display: none; }
+input.tab + label.tab { display: block; }
+input.tab + label.tab::before { content: '\25b8 \00a0'; float: left;}
+input.tab:checked + label.tab::before { content: '\25be \00a0'; }
+input.tab + label.tab + div.tab { display: none; }
+input.tab:checked + label.tab + div.tab { display: block; }
+
+input.color { display: none }
+input.color + label{
+  display: inline-block;
+  width: 1em; height: 1em;
+  border: 1px solid black;
+}
+input.color:checked + label{ border-width: 3px;}
+input.color[value="#000"] + label,
+input.color[value="#888"] + label { background-color: #888;}
+input.color[value="#00A"] + label { background-color: #00F;}
+input.color[value="#0A0"] + label { background-color: #0F0;}
+input.color[value="#0AA"] + label { background-color: #0FF;}
+input.color[value="#A00"] + label { background-color: #F00;}
+input.color[value="#A0A"] + label { background-color: #F0F;}
+input.color[value="#AA0"] + label { background-color: #FF0;}
+input.color[value="#FFF"] + label { background-color: #FFF;}
+
+h1, label.tab, div.tab, fieldset.tab,
+div.patient, div.prescription {
+  display: block;
+  width: 96%;
+  margin: 0 2%;
+}
+
+div.prescription span {
+  display: inline-block;
+  width: 50%;
+  margin-right: -.75ex;
+  vertical-align: top;
+}
+div.prescription span label { font-weight: bold; margin-right: 1ex;}
+div.prescription span.prescno,
+div.prescription span.catalogue {
+  width: 33%;
+  font-weight: bold;
+  margin-bottom: .5em;
+  padding: .5ex 1ex;
+}
+
+div.prescription ul {margin-top: 0;}
+
+div.prescription label.checkbox,
+div.prescription label.radio {
+  display: block;
+  padding-left: 1.25em;
+  font-size: 1em;
+  margin: .5em 0;
+}
+
+div.prescription label.checkbox:before,
+div.prescription label.radio:before {
+  display: inline-block;
+  color: #000;
+  background-color: #FFF;
+  height: 1.375em; width: 1.125em;
+  padding: .125em 0 0 .375em;
+  margin: 0 .5em .25em -1.25em;
+  border: 1px solid #000;
+  vertical-align: middle;
+  content: ' ';
+}
+div.prescription label.radio:before { border-radius: .5em;}
+div.prescription label.checkbox.checked:before,
+div.prescription label.radio.checked:before { content: "\2713";}
+
+div.prescription label[for=prescreviewed] {
+  margin-left: 1ex;
+  font-weight: bold;
+  text-decoration: underline;
+  background-color: #FCC;
+}
+div.prescription label[for=prescreviewed].checked {
+  font-weight: normal;
+  text-decoration: none;
+  background-color: transparent;
+}
+
+div.prescription label.tab {width: 96%; border: none; border-bottom: 1px dotted;}
+div.prescription div.tab { width: 96%; background-color: #DDD;}
+
+input.stickynote { display:none; }
+input.stickynote + .stickynote {
+  position: fixed;
+  background-color: #FF8;
+  top: 4em; bottom: 4em;
+  left: -4.5em; width: 5em;
+  padding: 1ex;
+  max-height: 90%;
+  z-index: 2;
+}
+input.stickynote + .stickynote:nth-of-type(2n) {
+  background-color: #8FF;
+  top: 8em;
+}
+
+input.stickynote + .stickynote > * { display: none; }
+input.stickynote + .stickynote > label {
+  position: absolute;
+  top: 0; bottom: 0; right: .5ex;
+  display: block;
+  text-align: right;
+  font-weight: bold;
+}
+input.stickynote + .stickynote:hover {
+  left: -1ex;
+}
+input.stickynote:checked + .stickynote {
+  width: auto; left: 1em; right: 1em;
+}
+input.stickynote:checked + .stickynote > * { display: block; }
+input.stickynote:checked + .stickynote > textarea {
+  display: block;
+  position: absolute;
+  left; 0; right: 0; bottom: 0; top: 0;
+  width: 100%; height: 100%;
+  background-color: #FF8;
+  padding: 2em 1em;
+}
+input.stickynote:checked + .stickynote > button[type="submit"] {
+  display: block;
+  position: absolute;
+  right: .5ex; bottom: .5ex;
+  z-index: 2;
+}
+input.stickynote:checked + .stickynote > label {
+  display: block;
+  position: static;
+  font-size: 0;
+}
+input.stickynote:checked + .stickynote > label:before {
+  position: absolute;
+  font-size: initial;
+  content: "x";
+  top: .5ex; right: .5ex;
+  padding: .125ex .75ex;
+  background-color: #000;
+  color: #FFF;
+  border-radius: 1ex;
+  z-index: 2;
+}
+
+fieldset.penwidth,
+fieldset.color {
+  position: absolute;
+  right: 0; width: 2em;
+  margin: .5em .5em .125em 2%;
+  border: none;
+  padding: 0;
+}
+fieldset.penwidth { bottom: 19em; }
+fieldset.penwidth > input {display: none;}
+fieldset.penwidth > input + label { display: none;}
+fieldset.penwidth > input:checked + label {
+  display: block;
+  width: 2em; height: 2em;
+  background-color: #000;
+  border: 1em solid #FFF;
+  border-radius: 1em;
+}
+fieldset.penwidth > input[value="4"]  + label { border-width: .75em; }
+fieldset.penwidth > input[value="12"] + label { border-width:  .5em; }
+fieldset.penwidth > input[value="36"] + label { border-width: .25em; }
+
+fieldset.color { bottom: 0; }
+fieldset.color > input.color + label {
+  width: 2em; height: 2em;
+}
+.dotmark {
+  max-width: 90%;
+  margin: .5em 1em .125em 2%; padding: 0;
+  text-align: left;
+  border: 1px solid black;
+}
+.dotmark.ov {
+  position: absolute;
+  left: 0; bottom: .25em;
+  z-index: 1;
+}
+
+@media(min-width: 800px){
+  h1, label.tab, div.tab, fieldset.tab,
+  div.patient, div.prescription {
+    width: 38%;
+    margin-right: 0;
+  }
+  input.stickynote:checked + .stickynote { right: 50%; }
+  fieldset.penwidth,
+  fieldset.color { position: fixed; }
+  .dotmark {
+    position: fixed;
+    max-width: 52%;
+    max-height: 98%;
+    right: 2em; bottom: .25em;
+  }
+  .dotmark.ov {
+    position: fixed;
+    right: 2em; left: auto;
+  }
+}
+
+h1 {display: none;}
+
+div.patient, div.prescription { margin-top: 1em; }
+div.prescription {
+  border: 1px solid black;
+  background-color: #EEE;
+  padding: .125em 1.25ex .5em 1.25ex;
+}
+div > h2 { margin: 0; border-bottom: 1px solid black; }
+div:nth-child(n+2) > a:first-of-type {
+  display: block;
+  margin: .125em 0 .5em 0;
+  text-decoration: none;
+}
+
+#report fieldset.tab,
+#report label.tab {
+  font-size: 1.25em;
+  font-weight: bold;
+  padding: .125em 1ex .25em 1ex;
+  color: #FFF;
+  background-color: #333;
+  margin-top: .125em;
+  text-align: right;
+  border: none;
+}
+#report label.heading {
+  background-color: #FFF;
+  margin-top: 1em;
+  border: 2px solid black;
+  border-bottom-width: 1px;
+  color: black;
+}
+#report label.heading > span {
+  text-decoration: underline;
+}
+
+#report fieldset.tab > *,
+#report label > input,
+#report label > span {
+  display: inline-block;
+  text-align: right;
+}
+#report .tab > .no {
+  width:  10%; float: left;
+  border: solid 1px #FFF;
+  background-color: #555;
+  border-radius: 2ex;
+  padding: 0;
+  text-align: center;
+}
+#report label.heading > span.no {
+  background-color: inherit;
+  border: none;
+}
+#report .tab > .date      { width: 30%; }
+#report .tab > .therapist { width: 30%; }
+#report .tab > .signature { width: 20%; }
+#report label.tab > .signature { font-size: .75em; }
+
+#report .signature > input[type=checkbox] {
+  display: inline;
+  font-weight: bold;
+  font-size: 1.25em;
+}
+#report .signature > input[type=checkbox]:before {
+  display: block; width: 1.25em;
+  margin: -.125em 0 0 -.5ex;
+  background-color: #FFF;
+  text-align: center;
+  content: "\00a0 \00a0 \00a0";
+}
+#report .signature > input[type=checkbox]:checked::before {
+  content: "\2713";
+}
+
+#report input.tab + label.tab > input.date,
+#report input.tab + label.tab > input.therapist {
+  display: none;
+}
+#report input.tab:checked + label.tab > input.date,
+#report input.tab:checked + label.tab > input.therapist {
+  display: inline;
+}
+#report input.tab:checked + label.tab > span.date,
+#report input.tab:checked + label.tab > span.therapist {
+  display: none;
+}
+
+#report div.tab {
+  border: 2px solid #333;
+  border-top-width: 1px;
+  margin-top: -1px;
+  padding: .25em .5ex 1em .5ex;
+}
+#report div.tab > fieldset.note {
+  border: none;
+  margin: 0; padding: 0;
+}
+#report div.tab > fieldset.note > textarea {
+  display: block;
+  width: 93%; height: 8em;
+  margin: -8em 0 .5em 2em;
+  font: normal 1em sans-serif;
+}
+div.tab > fieldset.note > input.color + label { margin: 0; display: block; }
+div.tab > fieldset.note > input.color[value="#888"]:checked ~ textarea { background-color: #AAA; }
+div.tab > fieldset.note > input.color[value="#00A"]:checked ~ textarea { background-color: #88F; }
+div.tab > fieldset.note > input.color[value="#0A0"]:checked ~ textarea { background-color: #8F8; }
+div.tab > fieldset.note > input.color[value="#0AA"]:checked ~ textarea { background-color: #8FF; }
+div.tab > fieldset.note > input.color[value="#A00"]:checked ~ textarea { background-color: #F88; }
+div.tab > fieldset.note > input.color[value="#A0A"]:checked ~ textarea { background-color: #F8F; }
+div.tab > fieldset.note > input.color[value="#AA0"]:checked ~ textarea { background-color: #FF8; }
+div.tab > fieldset.note > input.color[value="#FFF"]:checked ~ textarea { background-color: #FFF; }
+
+div.tab > button.delete {float: right; display: inline-block; margin-top: -1em; display: none;}
diff --git a/therapy/therapy.html.sh b/therapy/therapy.html.sh
new file mode 100755 (executable)
index 0000000..1821c24
--- /dev/null
@@ -0,0 +1,227 @@
+# Copyright 2016, 2017 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+t_session_note(){
+  session_n="$1"
+  note_n="$2"
+  
+  color=session${session_n}_note${note_n}_color
+  unset c0 c1 c2 c3 c4 c5 c6 c7
+  
+  if [ "$note_n" -eq 1 -o -n "${tpy[session${session_n}_note${note_n}]}" ]; then
+    printf '<input class="trailbtn" type="checkbox" checked="checked">'
+  else
+    printf '<input class="trailbtn" type="checkbox">'
+  fi
+
+  _checked="$(validate "$tpy[$color]" '#(888|00A|0A0|0AA|A00|A0A|AA0)' '#FFF')"
+       cat <<-EOF
+         <fieldset class="note trailbox">
+           <input class=color type=radio name="$color" value="#888" id=${color}_000 $(checked \#888)><label for=${color}_000></label>
+           <input class=color type=radio name="$color" value="#00A" id=${color}_001 $(checked \#00A)><label for=${color}_001></label>
+           <input class=color type=radio name="$color" value="#0A0" id=${color}_010 $(checked \#0A0)><label for=${color}_010></label>
+           <input class=color type=radio name="$color" value="#0AA" id=${color}_011 $(checked \#0AA)><label for=${color}_011></label>
+           <input class=color type=radio name="$color" value="#A00" id=${color}_100 $(checked \#A00)><label for=${color}_100></label>
+           <input class=color type=radio name="$color" value="#A0A" id=${color}_101 $(checked \#A0A)><label for=${color}_101></label>
+           <input class=color type=radio name="$color" value="#AA0" id=${color}_110 $(checked \#AA0)><label for=${color}_110></label>
+           <input class=color type=radio name="$color" value="#FFF" id=${color}_111 $(checked \#FFF)><label for=${color}_111></label>
+           <textarea name="session${session_n}_note${note_n}">${tpy[session${session_n}_note${note_n}]}</textarea>
+         </fieldset>
+       EOF
+}
+
+t_session(){
+  session_n="$1"
+
+  sid=session${session_n}
+
+  if [ "$tpy[${sid}_sigset]" = pos ]; then
+    sigcheck=checked
+  else
+    unset sigcheck
+  fi
+
+       cat <<-EOF
+         <input type=hidden name=$sid value=exists>
+         <input class="tab" type="checkbox" id=${sid}_open name="${sid}_open" value="checked" ${tpy[${sid}_open]}>
+         <label class="tab" for="${sid}_open">
+           <span class=no>${session_n}.</span><!--
+        --><span class=date>${tpy[${sid}_date]}</span><!--
+        --><input class="date" name="${sid}_date" value="${tpy[${sid}_date]}" placeholder="$(l10n date)"><!--
+        --><span class=therapist>${tpy[${sid}_therapist]}</span><!--
+        --><input class="therapist" name="${sid}_therapist" value="${tpy[${sid}_therapist]}" placeholder="$(l10n therapist)"><!--
+        --><span class=signature><input type="checkbox" name="${sid}_sigset" value="pos" $sigcheck></span>
+         </label>
+         <div class=tab>
+           <img class="dotmark ov" src="/therapies/${id%.tpy}_${sid}.png" alt="">
+           $(n=1;
+              while [ -n "${tpy[session${session_n}_note${n}]+x}" ]; do
+                if [ -n "${tpy[session${session_n}_note${n}]}" ]; then
+                  x=$n
+                fi
+                n=$(($n + 1))
+              done
+              for n in $(seq 1 $((${x:-0} + 3)) ); do t_session_note $session_n $n; done
+             )
+           <button class="trailbtn" type="submit">$(l10n trailsave)</button>
+           <button class=delete type=submit name=delete_session value=${session_n}>$(l10n delete_session)</button>
+         </div>
+       EOF
+}
+
+therapy_sessions(){
+  n=1; while [ -n "${tpy[session${n}]}" ]; do
+    t_session $n
+    n=$(($n+1))
+  done
+
+  sid=session$n
+
+       cat <<-EOF
+         <fieldset class="tab">
+           <button class=no type=submit name=new_session value=$sid>+</button><!--
+        --><input class=date name=${sid}_date value="" placeholder="$(l10n date)" /><!--
+        --><input class=therapist name=${sid}_therapist value="" placeholder="$(l10n therapist)" /><!--
+        --><span class=signature><span></span>
+           <input type=hidden name="${sid}_note1" value="">
+         </fieldset>
+       EOF
+}
+
+cat <<EOF
+<h1>$(l10n therapy)</h1>
+
+<div class="patient">
+  <h2>$client_name</h2>
+  <a href="?p=prescriptions&amp;client=${id%%.*}.vcf">&lt; $(l10n prescriptionlist)</a>
+</div>
+
+<div class="prescription">
+  <h2>$(l10n therapy_prescription)</h2>
+  <span class="insurance">${mpx[insurance]}</span>
+  <span class="date"><label>$(l10n date):</label>${mpx[date]}</span>
+
+  <label class="checkbox ${mpx[prescreviewed]:+checked}" for="prescreviewed">
+    $([ -n "${mpx[prescreviewed]}" ] && printf %s "$(l10n prescreviewed)" \
+                                     || printf %s "$(l10n prescreview)" )
+  </label>
+
+       ${mpx[prescno]:+<span class="prescno">$(l10n presc${mpx[prescno]})</span>}
+  ${mpx[grouptherapy]:+<span class="catalogue">$(l10n grouptherapy)</span>}
+     ${mpx[housecall]:+<span class="catalogue">$(l10n housecall)</span>}
+        ${mpx[report]:+<span class="catalogue">$(l10n report)</span>}
+
+  <ul>$(for n in '' {0..10}; do
+    [ -n "${mpx[remidy$n]}" ] && \
+    printf '<li>%s %s %s</li>' "${mpx[quantity$n]}" \
+                               "${mpx[remidy$n]}" \
+                               "${mpx[quantity_weekly$n]:+($mpx[quantity_weekly$n] $(l10n weekly))}"
+  done)</ul>
+
+  ${mpx[indicator]:+<span class="indicator"><label>$(l10n indicator):</label>${mpx[indicator]}</span>}
+      ${mpx[icd10]:+<span class="icd10">    <label>$(l10n icd10):</label>${mpx[icd10]}</span>}
+
+  ${mpx[addcontrib]:+
+  <label class='checkbox ${mpx[contribconfirm]:+checked}' for="addcontrib">
+    $([ -n "${mpx[contribconfirm]}" ] && printf %s "$(l10n contribconfirm)" ${mpx[contribconfirm]} \
+                                      || printf %s "$(l10n addcontrib)" )
+  </label>
+  }
+
+  <input class="tab" type="checkbox" id="indicator_reading">
+  <label class="tab" for="indicator_reading">$(l10n indicator_reading)</label>
+  <div class="tab">${mpx[indicator_reading]}</div>
+</div>
+
+<form id="report" method="POST" action="?action=update_therapy">
+  <input type="hidden" name="id" value="${id}">
+
+  <input class="stickynote" type="checkbox" name="c_stickynote" id="show_stickynote">
+  <fieldset class="stickynote">
+    <label for="show_stickynote">$(l10n notes)</label>
+    <h2>$(l10n notes)</h2>
+    <textarea name="stickynote">${tpy[stickynote]}</textarea>
+    <button type="submit">$(l10n save)</button>
+  </fieldset>
+
+  <input class="stickynote" type="checkbox" name="c_timesheet" id="show_timesheet">
+  <fieldset class="stickynote">
+    <label for="show_timesheet">$(l10n timesheet)</label>
+    <h2>$(l10n timesheet)</h2>
+    <table><thead>
+      <tr><th>$(l10n time_goal)</th><th>$(l10n time_actual)</th><th>$(l10n time_difference)</th></tr>
+    </thead><tbody>
+      $(for n in '' {0..10}; do
+        printf '<tr><td><input type="number" name="tsgoal" value="%s"/></td>
+                    <td><input type="number" name="tsactual" value="%s"/></td>
+                    <td>%s</td>
+                </tr>\n' \
+                "$mpx[tsgoal${n}]" "$mpx[tsactual${n}]" "$((${mpx[tsgoal${n}]:-0} - ${mpx[tsactual${n}]:-0}))"
+      done)
+    </tbody></table>
+    <button type="submit">$(l10n save)</button>
+  </fieldset>
+
+  <!--input class="tab" type="checkbox" id="lookout">
+  <label class="tab" for="lookout">$(l10n indicator_reading)</label>
+  <div class="tab"><textarea name="lookout">${mpx[lookout]}</textarea -->
+
+  <label class="tab heading">
+    <span class=no>$(l10n number)</span><!--
+ --><span class=date>$(l10n date)</span><!--
+ --><span class=therapist>$(l10n therapist)</span><!--
+ --><span class=signature>$(l10n signature)</span>
+  </label>
+EOF
+
+therapy_sessions
+
+_checked="$(validate "$tpy[penwidth]" '(4|12|36)' '4')"
+cat <<EOF
+  <fieldset class="penwidth">
+    <input type="radio" name="penwidth" value="4"  id="pw1" $(checked  4)><label for="pw2"></label>
+    <input type="radio" name="penwidth" value="12" id="pw2" $(checked 12)><label for="pw3"></label>
+    <input type="radio" name="penwidth" value="36" id="pw3" $(checked 36)><label for="pw1"></label>
+  </fieldset>
+EOF
+
+_checked="$(validate "$tpy[color]" '#(0[0A]{2}|A00|A0A|AA0|FFF)' '#000')"
+cat <<EOF
+  <fieldset class="color">
+    <input class="color" type="radio" name="color" value="#000" id="c000" $(checked \#000)><label for="c000"></label>
+    <input class="color" type="radio" name="color" value="#00A" id="c001" $(checked \#00A)><label for="c001"></label>
+    <input class="color" type="radio" name="color" value="#0A0" id="c010" $(checked \#0A0)><label for="c010"></label>
+    <input class="color" type="radio" name="color" value="#0AA" id="c011" $(checked \#0AA)><label for="c011"></label>
+    <input class="color" type="radio" name="color" value="#A00" id="c100" $(checked \#A00)><label for="c100"></label>
+    <input class="color" type="radio" name="color" value="#A0A" id="c101" $(checked \#A0A)><label for="c101"></label>
+    <input class="color" type="radio" name="color" value="#AA0" id="c110" $(checked \#AA0)><label for="c110"></label>
+    <input class="color" type="radio" name="color" value="#FFF" id="c111" $(checked \#FFF)><label for="c111"></label>
+  </fieldset>
+  <img class="dotmark bg" src="?static=therapy_background.png" alt="WARNING: Background Image not available!">
+  <canvas id="canvas" class="dotmark ov" width="${bg_dim%x*}" height="${bg_dim#*x}"></canvas>
+
+  <input type=hidden id=image_serialize name=imagedata value="">
+
+  <button type="submit">$(l10n save)</button>
+</form>
+
+<span id="jsdebug" style="display: none; position: fixed; right:0; bottom:0">Debug</span>
+
+<script type="text/javascript" src="?static=therapy_draw.js"></script>
+EOF
+
+# vi:set filetype=html:
diff --git a/therapy/therapy.sh b/therapy/therapy.sh
new file mode 100755 (executable)
index 0000000..8885515
--- /dev/null
@@ -0,0 +1,56 @@
+#!/bin/zsh
+
+# Copyright 2016 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+declare -A tpy
+declare -A mpx
+
+BR='
+'
+
+id="$_GET[id]"
+
+tpyfile="$_DATA/therapies/$id"
+mpxfile="$_DATA/prescriptions/${id%.tpy}.mpx"
+client="$_DATA/vcard/${id%%.*}.vcf"
+
+identify "$_EXEC/static/therapy_background.png" \
+| sed -r 's;^.* ([0-9]+x[0-9]+) .*$;\1;' \
+| read bg_dim
+
+card_N="$(sed -nr 's:^(N)(;[^"\:]+|;"[^"]+")*\:(.*)$:\3:gp' "$client")"
+card_FN="$(sed -nr 's:^(FN)(;[^"\:]+|;"[^"]+")*\:(.*)$:\3:gp' "$client")"
+card_NICK="$(sed -nr 's:^(NICKNAME)(;[^"\:]+|;"[^"]+")*\:(.*)$:\3:gp' "$client")"
+
+n=$(printf %s "$card_N" \
+    | sed -rn 's:^([^;]*)(\;[^;]*)(\;[^;]*)?(\;[^;]*)?(\;[^;]*)?$:\4 \2 \3 \1 \5:gp' \
+    | sed -r 's:,: :;s:\;: :g;s: +: :g;s:^ $::;'
+   )
+client_name="${n:-${card_FN:-${card_NICK}}}"
+
+mpx=()
+cat "$mpxfile" |while read -r line; do
+  val="${line#*:}"
+  mpx[${line%%:*}]="$(htmlsafe "${val//\\n/$BR}")"
+done
+
+tpy=()
+cat "$tpyfile" |while read -r line; do
+  val="${line#*:}"
+  tpy[${line%%:*}]="$(htmlsafe "${val//\\n/$BR}")"
+done
diff --git a/therapy/therapy_background.png b/therapy/therapy_background.png
new file mode 100644 (file)
index 0000000..a0574d5
Binary files /dev/null and b/therapy/therapy_background.png differ
diff --git a/therapy/therapy_background.xcf b/therapy/therapy_background.xcf
new file mode 100644 (file)
index 0000000..e69c024
Binary files /dev/null and b/therapy/therapy_background.xcf differ
diff --git a/therapy/therapy_draw.js b/therapy/therapy_draw.js
new file mode 100644 (file)
index 0000000..8a8e936
--- /dev/null
@@ -0,0 +1,115 @@
+// Copyright 2016 Paul Hänsch
+//
+// This file is part of Confetti.
+// 
+// Confetti is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+// 
+// Confetti is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+// 
+// You should have received a copy of the GNU Affero General Public License
+// along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+body = document.body
+dbg = document.getElementById("jsdebug")
+canvas = document.getElementById("canvas")
+data=document.getElementById("image_serialize")
+
+image = canvas.getContext("2d")
+mouse = 0
+image_serialize=""
+
+// start and current coordinates of a draw
+// serves for tracking, whether path ends close to its beginning
+stx=0, sty=0
+cux=0, cuy=0
+
+function setstroke(w) {
+  image.lineWidth = w
+  data.value += " stroke-width " + image.lineWidth
+}
+function setcol(c) {
+  this.c = c
+  image.strokeStyle = c
+  image.fillStyle =  c
+  data.value += " stroke " + c + "F"
+}
+
+function relX(x){
+  if ( body.clientWidth >= 800 ){
+    return Math.floor(cscaleW * (x - canvas.offsetLeft))
+  } else { 
+    return Math.floor(cscaleW * (x - canvas.offsetLeft + window.pageXOffset))
+  }
+}
+function relY(y){
+  if ( body.clientWidth >= 800 ){
+    return Math.floor(cscaleH * (y - canvas.offsetTop))
+  } else { 
+    return Math.floor(cscaleH * (y - canvas.offsetTop + window.pageYOffset))
+  }
+}
+
+function draw(x, y) {
+  if ( mouse == 1){
+    cux=relX(x), cuy=relY(y)
+
+    image.lineTo( cux, cuy )
+    image.stroke()
+
+    image_serialize += " " + cux + "," + cuy
+  }
+}
+
+function drawstart(x, y) {
+  mouse = 1
+
+  cscaleW = canvas.width / canvas.clientWidth
+  cscaleH = canvas.height / canvas.clientHeight
+
+  stx=relX(x), sty=relY(y)
+
+  setstroke(document.querySelector('input[name="penwidth"]:checked').value);
+  setcol(document.querySelector('input[name="color"]:checked').value);
+
+  image.beginPath()
+  draw(x, y)  // why must this not use relative Coords ???
+
+  image_serialize = " polyline"
+}
+
+function drawstop() {
+  // if path ends close to beginning ( < 50 px); then close path and fill
+  if ( false && mouse == 1 && Math.sqrt( Math.pow(stx - cux, 2) + Math.pow(sty - cuy, 2)) <= 50 && c !== "#FFF" ){
+    image.lineTo( stx, sty )
+    image.stroke()
+
+    image.globalAlpha = .5
+    image.fill()
+    image.globalAlpha = 1
+
+    image_serialize += " " + stx + "," + sty
+    data.value += " fill " + c + "8" + image_serialize
+  } else if (mouse == 1)  {
+    data.value += " fill #0000 " + image_serialize
+  }
+  dbg.innerHTML = " stx: " + stx + " cux: " + cux + " sty: " + sty + " cuy: " + cuy
+  image.closePath()
+  image_serialize = ""
+  mouse = 0
+}
+
+window.addEventListener( 'mouseup',   function()   { drawstop() } )
+canvas.addEventListener( 'mousedown', function(e)  { drawstart(e.clientX, e.clientY) } )
+canvas.addEventListener( 'mousemove', function(e)  {      draw(e.clientX, e.clientY) } )
+
+window.addEventListener( 'touchend',   function()  { drawstop() } )
+canvas.addEventListener( 'touchstart', function(e) { drawstart(e.touches[0].clientX, e.touches[0].clientY) } )
+canvas.addEventListener( 'touchmove',  function(e) { e.preventDefault(); draw(e.touches[0].clientX, e.touches[0].clientY) } )
diff --git a/therapy/update_therapy.sh b/therapy/update_therapy.sh
new file mode 100755 (executable)
index 0000000..6ba06cb
--- /dev/null
@@ -0,0 +1,70 @@
+#!/bin/zsh
+
+# Copyright 2016 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+BR='
+'
+tpy="${_POST[id]}"
+
+tpyfile="$_DATA/therapies/$tpy"
+tempfile="$_DATA/temp/$tpy"
+
+# serialize POST array into file
+for key in ${(k)_POST}; do
+  [ "$key" != imagedata ] && printf %s:%s\\n "$key" "${_POST[$key]//$BR/\\n}"
+done >"$tempfile"
+
+if [ -n "$_POST[delete_session]" ]; then
+  n="$_POST[delete_session]"
+  sed -i -r '/^session'$n'[_:]/d' "$tempfile"
+  rm "${tpyfile%.tpy}_session${n}.png"
+
+  while grep -Eq '^session'$(($n + 1))'_' "$tempfile"; do
+    sed -i -r 's;^session'$(($n + 1))'(_|:);session'$n'\1;' "$tempfile"
+    mv "${tpyfile%.tpy}_session$(($n+1)).png" "${tpyfile%.tpy}_session${n}.png"
+    n=$(($n+1))
+  done
+
+elif [ -n "$_POST[new_session]" ]; then
+  sid="$_POST[new_session]"
+
+  identify "$_EXEC/static/therapy_background.png" \
+  | sed -r 's;^.* ([0-9]+x[0-9]+) .*$;\1;' \
+  | read dim
+
+  convert -size "$dim" xc:transparent "${tpyfile%.tpy}_${sid}.png"
+
+  printf '%s:exists\n' "$sid" >>"$tempfile"
+  printf '%s_open:checked\n' "$sid" >>"$tempfile"
+
+elif [ -n "$_POST[imagedata]" ]; then
+  sed -rn 's;^(session[0-9]+)_open:checked$;\1;p' "$tempfile" \
+  | sort -n \
+  | tail -n1 \
+  | read sid
+
+  convert "${tpyfile%.tpy}_${sid}.png" \
+          -strokewidth 2 -fill '#00000000' \
+          -draw "${_POST[imagedata]}" -transparent white \
+          "${tpyfile%.tpy}_${sid}.png"
+  sync
+fi
+
+mv "$tempfile" "$tpyfile"
+
+redirect "?p=therapy&id=${tpy}"
diff --git a/update_bookmarks.sh b/update_bookmarks.sh
new file mode 100755 (executable)
index 0000000..1670840
--- /dev/null
@@ -0,0 +1,33 @@
+#!/bin/zsh
+
+# Copyright 2017 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+bmfile="${_DATA}/mappings/bookmarks"
+ url="$(validate "${_POST[bm_url]}" '/.+' '/')"
+name="$(validate "${_POST[bm_name]}" '.+' "$url")"
+
+case "${_POST[submit]}" in
+  add) printf '%s\t%s\n' "${url}" "${name}" >>"${bmfile}"
+    ;;
+  del) cp "${bmfile}" "${bmfile}.temp"
+       grep -vF "${url}        ${name}" "${bmfile}.temp" >"${bmfile}"
+       rm "${bmfile}.temp"
+    ;;
+esac
+
+redirect "${url}#CONFIGURE"