Thursday 18 December 2014

ughjhhjh


determine WebPAS message type;

In MSH segment check the 9th field, this is meant to contain

<message type (ID)> ^ <trigger event (ID)>

(see https://www.hl7.org/documentcenter/public_temp_B5A64101-1C23-BA17-0C953B25BD420909/wg/conf/HL7MSH.htm)

our PAS is a knob and has it like this instead:

Message
Description
Message Type
A01
Admit a Patient
ADT^A01^ADT_A01
A02
Transfer a Patient
ADT^A02^ADT_A02
A03
Discharge a Patient
ADT^A03^ADT_A03
A05
Pre-admit a Patient
ADT^A05^ADT_A01
A08
Update patient Information
ADT^A08^ADT_A01
A11
Cancel Admit
ADT^A11^ADT_A09
A12
Cancel Transfer
ADT^A12^ADT_A12
A13
Cancel Discharge
ADT^A13^ADT_A01
A14
Pending Pre-Admission
ADT^A14^ADT_A01
A21
On Leave
ADT^A21^ADT_A02
A22
Return from Leave
ADT^A22^ADT_A02
A27
Cancel Pending Pre-Admission
ADT^A27^ADT_A02
A28
Add Person Information
ADT^A28^ADT_A28
A31
Update person Information
ADT^A31^ADT_A01
OR
ADT^Axx^ADT_01 when using alternate “A” message
A34
Merge Person Information (Patient ID Only)
ADT^A34^ADT_A30
A44
Change U/R for a Visit
ADT^A44^ADT_A43
M02
Practitioner Message
MFN^M02^MFN_M02
S12
Notification of New Appointment Booking
SIU^S12^SIU_S12
S14
Notification of Appointment Modification
SIU^S14^SIU_S12
S15
Notification of Appointment Cancellation
SIU^S15^SIU_S12

Just check the first two fields.  The second appears to indicate the event that raised the message, for example admitting someone in PAS (admission event = A01) may send out:

preadmit message ADT^A05^ADT_A01
then update patient info ADT^A31^ADT_A01
then admission message ADT^A01^ADT_A01

The ADT events we mostly care about:

A01 Admit / visit notification
A02 Transfer a patient
A03 Discharge/end visit
A04 Registration (e.g. ED visit where not admitted)
A05 Pre-admit a patient
A08 Update patient information
A18 Merge patient information
A19 QRY/ADR - Patient query
A31 Update person information
A34 Merge patient information - patient I

full list


Sunday 14 December 2014

The ED Whiteboard thing

Ok, here is the ED whiteboard* overview:

(* calling it a whiteboard is kind of misleading, it's just called that because it replaces a real whiteboard that people draw on with those nice smelling pens I covertly sniff while in meetings.  It's probably better described as a ward & patient overview screen)

There are a heap of boxes. Each box represents a bed. These are laid out the same as the ED ward (i.e. this is basically a map of the ED).  These run on two 47 inch touchscreen monitors in the ward.

You are thrilled I am sure.

Reasons for having a map view rather than a big ol' table/grid:

  • Can see wards status (e.g. occupancy) at a glance
  • Easy to locate empty beds or areas that normally shouldn't be filled (overflow areas)
  • Groups a patient's data together rather than smearing it across the page in a giant table 
  • It's in the requirements :/

The big list of patients is all the folks who have been through triage and are in the PAS.  You smush your greasy finger against one of these patients and then smush it against one of the empty boxes.  This moves the patient!  Touchscreens huh, truly we are living in the future.

When the patient is in a room (or 'location', cause they aren't all rooms; some are chairs, some are just empty spots in the corridor (I am actually not joking)) you can see some more data about that patient:

It sure looks like a mess doesn't it.  It all is Very Important Information that I can't be bothered 'splaining right now.  Additionally, if you then smush your finger against a patient twice it will open a pop-up window:

Because doctors like to order things and have data their own way, the same data from the above is accessible through the clinical portal (concerto) in a grid:

Because I hate grids I made it not really look like a grid and instead look like a stack of sausages because I do like sausages.

Reasons for having a grid:

  • Access the data through portal - cramming a map view on a desktop screen is madness
  • You can order it however you want, e.g. show most urgent triage value at top

Click columns to sort the grid, or do advanced sorting which is still just clicking columns, but more than one column

And in a travesty of design you can also open the filters which should totally be attached to the column headers but aren't because I like to reinvent the wheel and confuse the users and also partly because I ran out of time



Wednesday 12 November 2014

Scheduling a service to stop and start


I lol at the vendor windows service that dies but appears to still be running from the services window.

  1. Create a stupid bat file to stop the task with the contents of
    NET STOP "ServiceNameGoesHere"
  2. Create a stupid bat file to start the task with the contents of
    NET START "ServiceNameGoesHere"
  3. Create a scheduled task to run the first bat.
  4. Create a scheduled task to run the second bat a few minutes later.
  5. Does this really need a post, am I really going to forget this? Probably.
Notes:
  • Use a domain user to run the tasks because it just doesn't want to work otherwise
  • Make sure the user has the log on as batch thing (http://www.power-programming.co.uk/post/2010/11/18/Task-Scheduler-This-task-requires-that-the-user-account-specified-has-Log-on-as-batch-job-rights.aspx)
  • The dudes on the internet reckon you might need to make the user an admin (I didn't)
  • The dudes on the internet reckon you might need to explicitly give the user Full Access to the folder and .bat even if they are an admin (I didn't)

Tuesday 11 November 2014

Sarcasm

Landing pages for the patient administration system:





CSC, we love you too.

Tuesday 12 August 2014

Noah PC desktop is sad

So, how do you feel about having to install all these different bits of software on the Noah PC?



Wednesday 6 August 2014

Config for getting GitHub going at work that I always forget

GitHub, you make me so :/

How to set proxy:

git config --global http.proxy http://user:password@proxyname:port

How to unset proxy:

git config --global --unset http.proxy

Stop the 'fatal: unable to access 'https:github.com/[whatever]/' : SSL certificate problem: unable to get local issuer certificate::

git config -- global http.sslVerify false




Thursday 8 May 2014

If you give people options they will choose the wrong one


We started with a tick icon and it was rejected because people might get confused thinking a tick means you're in the office, not on leave:


These options were then presented:

1. A cross, like you know, a calendar date crossed out:




2. A generic exit icon:




3. The head of Macho Man Randy Savage:



They did not pick the Macho Man.  They didn't pick Macho Man!!!

Thursday 10 April 2014

There was a problem starting C:\Program

 

I've had this a few times recently when trying to uninstall something from Control Pannels -> Uninstall or change a program.

The uninstall is trying to run something in program files and is choking on the space in the path. So the problem is coming from a path somewhere which doesn't have quotes around it. Bunch of rookies.

How to find what the uninstall is actually running? It's in the registry. Depending on whether it is x86 or x64 it'll be in one of these locations:

HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall

In each of those locations there will be a heap of folder. Go through them till you find the one for your application - you can check the DisplayName value for this.

When you find the application you are trying to uninstall locate the 'UninstallString' value

This will look something like:

RunDll32 C:\Program Files (x86)\Common Files\InstallShield\Professional\RunTime\11\50\Intel32\Ctor.dll,LaunchSetup "C:\Program Files (x86)\InstallShield Installation Information\{0363D88C-A222-4715-AE00-76AC8DB8C377}\setup.exe" -l0x9  -removeonly

It needs to have quotes around path directly after RunDll32.  Like this:

RunDll32 "C:\Program Files (x86)\Common Files\InstallShield\Professional\RunTime\11\50\Intel32\Ctor.dll",LaunchSetup "C:\Program Files (x86)\InstallShield Installation Information\{0363D88C-A222-4715-AE00-76AC8DB8C377}\setup.exe" -l0x9  -removeonly

You can either update the registry setting and run the uninstaller from the control panel window again or just take the updated string and run it from a cmd window.


Wednesday 12 March 2014

Webstock 2014, was all like, weeeeeeee.

http://www.webstock.org.nz/14/

Prepare yourself for basic facts:

  • two day conference for around $1000, couple of (optional) days of workshops beforehand for additional $$ 
  • heavily design focused - this is not a technical conference. You get the good stuff like motivation, inspiration and entertainment, but you prob won't learn new skills or hear about new technology.
  • single stream, so no hard decisions for you over what to see.
  • there is free ice cream.
  • occasionally there is free beer, but it is too hard to get because the line is ridiculous. But it's craft beer anyway, so don't be too upset.


Some of the presentations I particularly liked were:

  • Tom Loosemore talking about UK govs digital plans - trashing a whole lot of existing stuff that isn't working, getting a good work culture, a team that has the power to change things, making products that people want. 
  • Dan Saffer - microinteractions. Little basic things that applications do, how they can impact the user experience & be memorable or disastrous.
  • Charlie Todd - Improv everywhere. The stuff these dudes do is pretty funny so this was very entertaining.
  • Sha Hwang - When it's out just go watch it.

There are some photos here: http://www.flickr.com/photos/webstock06/

My rating, I give this conference 5 custom fonts out of a possible 5 custom fonts.

I sure do like bullet points.

Sunday 23 February 2014

SSIS vs Oracle - Something bad happened but no one's telling you what

Trying to run an SSIS 2012 package that accesses an Oracle database through ODP.net.  It works when running the package through Visual Studio, but fails once deployed with the error:

The type initializer for 'Oracle.DataAccess.Client.OracleConnection' threw an exception.


Ok, something bad happened. There may be an internal exception here, but I guess SSIS isn't going to tell us what it is.

This is complicated by the fact that every installer that Oracle has ever built is pretty much rubbish.

Did it install correctly?
Can't the installer tell me some sort of 'success' or 'failure' rather than just closing?
Is that an information message or an error message?
Where did all the files go?
What do I currently have installed?
Why is there no entry in the install/uninstall programs control panel?
Why is it getting so confused over x86 and x64?
What files is it actually trying to use?
Why are we still using .bat files for an install it's not 1996 anymore?
Why can't I uninstall this, the uninstaller does nothing!?

Apparently the error is sometimes related to x64 trying to access x86 and vice-versa. I had some type of ODP.net installed from months ago, but wasn't sure what version or whether it was x64 or x86. Couldn't figure out how to find that info out. I tried installing both x64 and x86 versions of ODP.net but due to the installers was unable to tell if they installed correctly and still had the above error anyway.

What did work was when executing the deployed package from the SSIS server you can tell it to use the 32-bit runtime:


So I dunno. I guess this means I have the x86 version of ODP.net installed. This is A Good Thing To Know.

Wednesday 29 January 2014

Notes on accessing AtTask REST service for Project Status reporting

AtTask is used as our project management software. The REST service for it lives here:

https://[yourcompanynamehere].attask-ondemand.com/attask/api/

The documentation is here.

To get info on projects:

https://[yourcompanynamehere].attask-ondemand.com/attask/api/project/search?



To get info on tasks:

 https://[yourcompanynamehere].attask-ondemand.com/attask/api/task/search? will return task information.

To get all of the fields for a project you can add 'field=*' to the query string.

https://[yourcompanynamehere].attask-ondemand.com/attask/api/project/search?fields=*


To just get specific fields pass the field names as comma separated.

https://[yourcompanynamehere].attask-ondemand.com/attask/api/project/search?fields=actualCost,actualCompletionDate


To filter put the field name and the value you want:

https://[yourcompanynamehere].attask-ondemand.com/attask/api/project/search?status=CUR


From .net you can use the System.Net.Http.HttpClient object to call the above URLs. You need to tell the HttpClient object that it's cool to get back a JSON response so you need to add a handler to it, something like this:

var proxy = new WebProxy
                {
                    Address = new Uri(proxyUri),
                    Credentials = CredentialCache.DefaultNetworkCredentials
                };

var handler = new HttpClientHandler
                    {
                        PreAuthenticate = true,
                        ClientCertificateOptions = ClientCertificateOption.Automatic,
                        UseProxy = true,
                        Proxy = proxy
                    };

var client = new HttpClient(handler) {BaseAddress = new Uri(baseUrl)};

// make it so we accept json as a response
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

HttpResponseMessage response = client.GetAsync(url).Result;


Then use the System.Web.Script.Serialization.JavaScriptSerializer to map the JSON into .Net objects:

var serializer = new JavaScriptSerializer();

var projects = serializer.Deserialize<IList<Project>>(json);

Where your Project object contains all the fields you are expecting from the JSON

public class ProjectDto
{
    public string ID { get; set; }
    public string name { get; set; }
    public string objCode { get; set; }
    public string percentComplete { get; set; }
    public string plannedCompletionDate { get; set; }
    public string plannedStartDate { get; set; }
    public string priority { get; set; }
    public string projectedCompletionDate { get; set; }
    public string status { get; set; }

}


You can use the above data to make reports based on real time project status:



Sunday 26 January 2014

A Chrome Extension to alter a pages HTML using JQuery on button click


This is a chrome extension that will add a button to the browser next to the URL bar; When the button is clicked it will search the content of the current page and make any tweets it finds on the page much better.

The code is here:
https://sites.google.com/site/kpdownloadz/downloads/tweet%20improver.zip?attredirects=0&d=1

How to use this extension: I am too cheap to pay for the developer license which lets you upload it to the Chrome Web Store so you have to download it and then load it into the browser manually.

  1. Download the above zip and unpack to your PC
  2. Open Chrome and put 'chrome://extensions/' into a browser window
  3. Make sure 'Developer mode' is ticked
  4. Press 'Load unpacked extension...' and select the folder where you unpacked the above zip
  5. A little TI icon will show up to the right of the URL bar which you can click on when visiting twitter sites



There's a bit of clowning around to get access to jquery, so this is my documentation for the process. NOTE: I was a BAD DEVELOPER and didn't read the documentation; this was all copy and paste and guess, so some of it is probably a bit wrong. Anyway, you'll end up with all of these files:



1. Start with the manifest.json. This describes the extension.


{
  "name": "Tweet Improver",
  "description": "This will fix all your dumb tweets",
  "version": "2.0",
  "permissions": [
    "activeTab"
  ],
  "background": {
    "scripts": ["jquery-2.1.0.min.js","background.js"],
    "persistent": false
  },
  "browser_action": {
    "default_title": "Improve your dumb tweets now",
"default_icon": {
      "19": "TI19.png",
      "38": "TI38.png"
    }
  },
  "icons": { "16": "TI16.png",
           "48": "TI48.png",
          "128": "TI128.png" },
  "manifest_version": 2,
  "content_scripts": [ {
    "js": [ "jquery-2.1.0.min.js", "background.js" ],
    "matches": [ "http://*/*", "https://*/*"]
  }]
}

For the background item we give it two scripts, we give it jquery first as apparently it loads them in that order.  Then we give it background.js, which is the main file being called by the browser action. These files need to be locally in your extension folder.

The browser_action bit is where we describe what our little button is going to look like.  We give it a couple of icons and some text that will show as a tool tip.

The icons bit describes the icons used for the chrome://extensions page and I think also the extensions downloads page (dunno, I never put mine up there).

The content_Scripts bit says that we want jquery injected into our background.js page, so in theory we should be able to use jquery when writing code in background.js.

2. Make a background.js


This is the js file our extension is hooked up to:

chrome.browserAction.onClicked.addListener(function(tab) {

  chrome.tabs.executeScript({
file: 'doittoit.js'
  });

});


Ok, there isn't much here. We're saying that when our browserAction icon thing is clicked we want to execute the js in the file doittoit.js.  

3. Your big mess of custom javascript to manipulate the DOM


You can have whatever you want in here - you've got access to all the jquery stuff.

  var tweetText = $(".tweet-text");

  tweetText.each(function() {
var newText = "";
var currentText = $(this).text();
if(currentText.length < 4) {
newText = "8=D";
} else {
newText = "8" + Array(currentText.length-1).join("=") + "D";
}
    $(this).text(newText);
  });

  $('.js-media-container').html('');

We do a search for any elements that have the class 'tweet-text' and then replace the text of that element to something that was better than the original. We also clear out any pictures (by setting the HTML of 'js-media-container' elements to nothing).




Wednesday 8 January 2014

Save attachments from an Exchange mail box using the EWS Managed API

So from C# I wanted be able to save email attachments being sent to an Exchange mailbox. I bet you're all like 'good luck dude, that POP/imap/mapi stuff is terrible and really I can't believe no one has sorted out a good way to do it yet'.  Yeah well screw you, have you heard about the Exchange Web Service Managed API? It's pretty sweet. It's easy to setup, easy to use & pretty intuitive. We get to use predefined objects AND I LOVE OBJECTS. This is how to do it.

The summary version:
You get a dll off microsoft, reference it in your project, give it some authentication details. It will work out where your exchange server is (i.e. you don't need to tell it the server name/ip or manually add the EWS as a service reference to your project).  Then you get to mess around with the supplied objects from that dll to pull and push data from the exchange server.  When pulling back objects from the server (e.g. an email) you often get the ability to pull back just a little bit of info (e.g. the id) or to get more details (e.g. base properties or even specific properties).  If you make a change to an object you need to push that change back to the server by calling a method on that object (e.g. message.Update(...)).

The longer version:

First you need to download the Microsoft dlls here that will give you the nifty objects and handle all the client/server communication:

http://www.microsoft.com/en-nz/download/details.aspx?id=35371

This is an MSI.  But whatevs, it puts a couple of DLLs on your system that you can then copy and reference from your project:



First you've got to create your service object.  Supply it the schema definition that you want to work with.  From what I understand this is backwards compatible; for example if you hook it up to Exchange2010 SP1, when you move to SP2 your code against the SP1 schema will still work fine, but you have the option of updating your code to run against SP2 which will have more functionality. The below code uses Microsoft Black Magic® to figure out where your exchange server is and how to talk to it's associated EWS.

static ExchangeService _service = new ExchangeService(ExchangeVersion.Exchange2010_SP2); 

static Program() 

    _service.Credentials = new WebCredentials("myusername", "password");
    _service.AutodiscoverUrl("my.email@somewhere.com"); 


Next, get a reference to a specific folder in the Exchange mailbox.  The below method takes a folder name and searches in the inbox for that folder. This uses the FindFolders method on the service object. To use it just give it a starting point (e.g. the inbox or the root) a search filter (an object that describes what we're looking for) and a view (an object that describes what we want returned e.g. what properties we require on the results objects).

public static Folder GetFolderByName(string folderName)
{
    FolderView view = new FolderView(10);
    view.PropertySet = new PropertySet(BasePropertySet.IdOnly); // just give us the id in the results object
    view.PropertySet.Add(FolderSchema.DisplayName); // on second thoughts, also give us the folder name
    view.Traversal = FolderTraversal.Deep; // search sub folders

    SearchFilter searchFilter = new SearchFilter.IsEqualTo(FolderSchema.DisplayName, folderName);

    FindFoldersResults findFolderResults = _service.FindFolders(WellKnownFolderName.Inbox, searchFilter, view);
    if(findFolderResults.Count() > 1)
        throw new Exception(string.Format("Found {0} folders with title '{1}' - the name needs to be unique; don't know which one to use", findFolderResults.Count(), folderName));

    return findFolderResults.FirstOrDefault();
}



Once you've got a folder you can find the unread emails in that folder.  This runs a search similar to the above, calling the FindItems method on the service object.  Pass it the folder id you want to search in and a filter (describing what we're looking for) and a view (describing what we want returned).  Then we do some clowning around to get the message as an EmailMessage type with some specific properties loaded (.Attachments and .HasAttachments) rather than the generic Item type.


public static List<EmailMessage> GetUnreadEmailsInFolder(Folder folder)
{
    var messages = new List<EmailMessage>();

    // only get unread emails
    SearchFilter folderSearchFilter = new SearchFilter.IsEqualTo(EmailMessageSchema.IsRead, false);
    // we just need the id in our results
    var itemView = new ItemView(10) {PropertySet = new PropertySet(BasePropertySet.IdOnly)};

    FindItemsResults<Item> findResults = _service.FindItems(folder.Id, folderSearchFilter, itemView);
         
    foreach (Item item in findResults.Items.Where(i => i is EmailMessage))
    {
        EmailMessage message = EmailMessage.Bind(_service, item.Id, new PropertySet(BasePropertySet.IdOnly, ItemSchema.Attachments, ItemSchema.HasAttachments));
        messages.Add(message);
    }

    return messages;
}


Now we have a list of emails.  Loop through them, get any attachments from the Exchange server and save them to disk.  When dealing with attachments it seems like you need to explicitly call the .Load() method of the attachment to pull it from Exchange.
To set the email as having been read just update the IsRead property and then call the .Update(...) method to push the change back to Exchange.

public static void ProcessEmail(EmailMessage message)
{
    string saveDir = ConfigurationManager.AppSettings["AttachmentSaveDirectory"];
    if (message.HasAttachments)
    {
        foreach (Attachment attachment in message.Attachments.Where(a=> a is FileAttachment))
        {
            FileAttachment fileAttachment = attachment as FileAttachment;
            fileAttachment.Load(); // populate the content property of the attachment

            using (FileStream fs = new FileStream(saveDir + attachment.Name, FileMode.Create))
            {
                using (BinaryWriter w = new BinaryWriter(fs))
                {
                    w.Write(fileAttachment.Content);
                }
            }
        }
    }
    message.IsRead = true;
    message.Update(ConflictResolutionMode.AutoResolve); // push changes back to server
}



Pack it up pack it in, jobs done.